clone

a desktop tv and program guide for over-the-air broadcasts: what's on air, what your tuner gets, live tv, recordings

main
airwaves / internal / api / tv_test.go
tv_test.go 3.7K
1package api23import (4 "io"5 "net/http"6 "net/http/httptest"7 "testing"8 "testing/fstest"9)1011// TestTV: the TV interface is at /tv/ without the API's token, pages and12// scripts revalidated on every load, fonts and libraries cached; the API13// it calls still wants the token.14func TestTV(t *testing.T) {15 files := fstest.MapFS{16 "index.html": {Data: []byte("<!doctype html><title>Airwaves</title>")},17 "js/core.js": {Data: []byte("'use strict';")},18 "fonts/plex.woff2": {Data: []byte("font")},19 "vendor/hls.min.js": {Data: []byte("hls")},20 "vendor/kenney/a.svg": {Data: []byte("<svg/>")},21 "vendor/kenney/b.svg": {Data: []byte("<svg/>")},22 "vendor/pretext/x.mjs": {Data: []byte("x")},23 }24 srv := httptest.NewServer((&Server{Backend: &fake{}, Token: "s3cret", TV: files}).Handler())25 defer srv.Close()26 noFollow := &http.Client{CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}27 get := func(path string, header ...string) (*http.Response, string) {28 t.Helper()29 req, _ := http.NewRequest("GET", srv.URL+path, nil)30 for i := 0; i+1 < len(header); i += 2 {31 req.Header.Set(header[i], header[i+1])32 }33 resp, err := noFollow.Do(req)34 if err != nil {35 t.Fatal(err)36 }37 defer resp.Body.Close()38 body, _ := io.ReadAll(resp.Body)39 return resp, string(body)40 }4142 for _, path := range []string{"/tv", "/"} {43 if resp, _ := get(path); resp.StatusCode != http.StatusFound || resp.Header.Get("Location") != "/tv/" {44 t.Errorf("%s: %d to %q", path, resp.StatusCode, resp.Header.Get("Location"))45 }46 }47 resp, body := get("/tv/")48 if resp.StatusCode != 200 || body != "<!doctype html><title>Airwaves</title>" {49 t.Fatalf("/tv/: %d %q", resp.StatusCode, body)50 }51 if cc, ct := resp.Header.Get("Cache-Control"), resp.Header.Get("Content-Type"); cc != "no-cache" || ct != "text/html; charset=utf-8" {52 t.Errorf("/tv/: Cache-Control %q, Content-Type %q", cc, ct)53 }54 etag := resp.Header.Get("ETag")55 if etag == "" {56 t.Fatal("/tv/: no ETag")57 }58 if resp, _ := get("/tv/", "If-None-Match", etag); resp.StatusCode != http.StatusNotModified {59 t.Errorf("/tv/ again: %d, want 304", resp.StatusCode)60 }61 if resp, body := get("/tv/js/core.js"); resp.StatusCode != 200 || body != "'use strict';" || resp.Header.Get("Cache-Control") != "no-cache" {62 t.Errorf("js/core.js: %d %q %q", resp.StatusCode, body, resp.Header.Get("Cache-Control"))63 }64 if resp, _ := get("/tv/js/core.js", "If-None-Match", etag); resp.StatusCode != 200 {65 t.Errorf("js/core.js with index.html's ETag: %d", resp.StatusCode)66 }67 for _, path := range []string{"/tv/fonts/plex.woff2", "/tv/vendor/hls.min.js", "/tv/vendor/kenney/a.svg"} {68 if resp, _ := get(path); resp.StatusCode != 200 || resp.Header.Get("Cache-Control") != "public, max-age=86400" {69 t.Errorf("%s: %d %q", path, resp.StatusCode, resp.Header.Get("Cache-Control"))70 }71 }72 for _, path := range []string{"/tv/vendor/", "/tv/vendor/kenney/", "/tv/missing.js"} {73 if resp, _ := get(path); resp.StatusCode != http.StatusNotFound {74 t.Errorf("%s: %d, want 404", path, resp.StatusCode)75 }76 }77 if resp, _ := get("/tv/index.html"); resp.StatusCode != http.StatusMovedPermanently || resp.Header.Get("Location") != "./" {78 t.Errorf("/tv/index.html: %d to %q", resp.StatusCode, resp.Header.Get("Location"))79 }80 if resp, _ := get("/api/info"); resp.StatusCode != http.StatusUnauthorized {81 t.Errorf("/api/info without the token: %d", resp.StatusCode)82 }8384 // Without the interface there's nothing at /tv/.85 bare := httptest.NewServer((&Server{Backend: &fake{}}).Handler())86 defer bare.Close()87 if resp, err := http.Get(bare.URL + "/tv/"); err != nil || resp.StatusCode != http.StatusNotFound {88 t.Errorf("no TV: %v %v", resp, err)89 }90}