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 / vchan / details_test.go
details_test.go 7.7K
1package vchan23import (4 "net/http"5 "net/http/httptest"6 "os"7 "path/filepath"8 "strings"9 "testing"10 "time"11)1213func TestParseNumber(t *testing.T) {14 // Numbers are kept as written: the user's "104.0" stays "104.0".15 for in, want := range map[string]string{16 "1.4": "1.4", " 1.4 ": "1.4", "104.0": "104.0", "1.0": "1.0", "01.004": "01.004", "999.999": "999.999", "12-3": "12.3",17 "0.4": "", "000.4": "", "1000.1": "", "1.1000": "", "1": "", "1.": "", "a.b": "", "1.2.3": "", "": "",18 } {19 got, ok := ParseNumber(in)20 if got != want || ok != (want != "") {21 t.Errorf("ParseNumber(%q) = %q, %v; want %q", in, got, ok, want)22 }23 }24 // They're the same number by value, however written.25 for _, c := range []struct {26 a, b string27 same bool28 }{{"1.05", "1.5", true}, {"104.0", "104.00", true}, {"1-2", "1.2", true}, {"104.0", "104.1", false}, {"1.1", "11.0", false}} {29 if SameNumber(c.a, c.b) != c.same {30 t.Errorf("SameNumber(%q, %q) = %v", c.a, c.b, !c.same)31 }32 }33}3435// TestLibraryKeepsNumbersAsWritten: the user's "104.0" folders load with36// their numbers as written, sort by value, and a number written another37// way doesn't get a second channel.38func TestLibraryKeepsNumbersAsWritten(t *testing.T) {39 root := t.TempDir()40 for _, d := range []string{"104.0 Cartoons", "105.0 Game Shows", "106.0 Marvel", "1.10 Ten", "1.2 Fish", "01.2 Clash", "1.01 Weather Too"} {41 if err := os.Mkdir(filepath.Join(root, d), 0o755); err != nil {42 t.Fatal(err)43 }44 }45 l := &Library{Root: root, Reserved: func() []string { return []string{"1.1"} }}46 var got []string47 for _, e := range l.Entries() {48 got = append(got, e.Channel.Number()+" "+e.Channel.Name())49 }50 // The first folder by name gets a number; "1.2 Fish" comes after51 // "01.2 Clash", and 1.01 is the weather channel's 1.1.52 want := "01.2 Clash|1.3 1.01 Weather Too|1.4 1.2 Fish|1.10 Ten|104.0 Cartoons|105.0 Game Shows|106.0 Marvel"53 if strings.Join(got, "|") != want {54 t.Errorf("got %q\nwant %q", strings.Join(got, "|"), want)55 }56}5758// touch gives a file a new modification time, so a rewrite of the same59// size is noticed however coarse the file system's clock.60func touch(t *testing.T, path string, d time.Duration) {61 t.Helper()62 at := time.Now().Add(d)63 if err := os.Chtimes(path, at, at); err != nil {64 t.Fatal(err)65 }66}6768func TestFolderDetails(t *testing.T) {69 dir := filepath.Join(t.TempDir(), "1.4 Cartoons")70 if err := os.Mkdir(dir, 0o755); err != nil {71 t.Fatal(err)72 }73 f := &Folder{Num: "1.4", Title: "Cartoons", Dir: dir}74 if d := f.Details(); d != (Details{Number: "1.4", Name: "Cartoons", Category: "Other", Enabled: true}) {75 t.Errorf("without channel.json: %+v", d)76 }7778 // A logo dropped in is found without being named.79 writeFile(t, filepath.Join(dir, "logo.svg"), "<svg/>")80 if d := f.Details(); d.Logo != filepath.Join(dir, "logo.svg") {81 t.Errorf("logo by name: %q", d.Logo)82 }8384 record := filepath.Join(dir, DetailsFile)85 writeFile(t, record, `{"callSign": " TOON ", "category": "kids", "description": "Saturday mornings.", "logo": "toon.png", "enabled": false}`)86 writeFile(t, filepath.Join(dir, "toon.png"), "png")87 d := f.Details()88 if d.CallSign != "TOON" || d.Category != "Kids" || d.Description != "Saturday mornings." || d.Enabled ||89 d.Logo != filepath.Join(dir, "toon.png") || d.Number != "1.4" || d.Name != "Cartoons" {90 t.Errorf("from channel.json: %+v", d)91 }9293 // Edits are read again; a category that isn't one, and a logo outside94 // the folder, are left out.95 writeFile(t, record, `{"category": "Cooking", "logo": "../toon.png"}`)96 touch(t, record, time.Second)97 if d := f.Details(); d.Category != "Other" || d.Logo != "" || !d.Enabled || d.CallSign != "" {98 t.Errorf("after an edit: %+v", d)99 }100 writeFile(t, record, `{"callSign": `)101 touch(t, record, 2*time.Second)102 if d := f.Details(); d.Category != "Other" || !d.Enabled || d.Logo != filepath.Join(dir, "logo.svg") {103 t.Errorf("unreadable channel.json: %+v", d)104 }105106 // Jellyfin and YouTube channels read theirs the same way.107 writeFile(t, record, `{"callSign": "NL"}`)108 touch(t, record, 3*time.Second)109 j := &Jellyfin{Num: "1.4", Title: "Cartoons", Config: filepath.Join(dir, jellyfinConfig)}110 y := &YouTube{Num: "1.4", Title: "Cartoons", Dir: dir}111 if j.Details().CallSign != "NL" || y.Details().CallSign != "NL" {112 t.Errorf("Jellyfin %+v, YouTube %+v", j.Details(), y.Details())113 }114}115116func TestWeatherRecord(t *testing.T) {117 if d := (&Weather{}).Details(); d.Number != "1.1" || d.Name != "Airwaves Weather" || d.CallSign != "WX" ||118 d.Category != "Weather" || d.Description == "" || !d.Enabled || d.Logo != "" {119 t.Errorf("without a record: %+v", d)120 }121 root := t.TempDir()122 wx := &Weather{Record: filepath.Join(root, WeatherFile)}123 if wx.Number() != "1.1" || wx.Name() != "Airwaves Weather" {124 t.Errorf("no record yet: %s %s", wx.Number(), wx.Name())125 }126 writeFile(t, wx.Record, `{"number": "2.05", "name": " Weather Now ", "callSign": "WTHR", "description": "The forecast.", "enabled": false}`)127 writeFile(t, filepath.Join(root, WeatherLogoName+".png"), "png")128 d := wx.Details()129 if d.Number != "2.05" || d.Name != "Weather Now" || d.CallSign != "WTHR" || d.Category != "Weather" ||130 d.Description != "The forecast." || d.Enabled || d.Logo != filepath.Join(root, ".weather-logo.png") {131 t.Errorf("from the record: %+v", d)132 }133 if wx.Number() != "2.05" || wx.Name() != "Weather Now" {134 t.Errorf("Number and Name: %s %s", wx.Number(), wx.Name())135 }136 writeFile(t, wx.Record, `{"number": "0.5", "name": " "}`)137 touch(t, wx.Record, time.Second)138 if wx.Number() != "1.1" || wx.Name() != "Airwaves Weather" {139 t.Errorf("a bad number and blank name keep the defaults: %s %s", wx.Number(), wx.Name())140 }141}142143func TestLibraryGivesWayToTheWeather(t *testing.T) {144 root := t.TempDir()145 for _, d := range []string{"1.2 Fish", "Birds", "1000.1 Too Big"} {146 if err := os.Mkdir(filepath.Join(root, d), 0o755); err != nil {147 t.Fatal(err)148 }149 }150 weather := "1.1"151 l := &Library{Root: root, Reserved: func() []string { return []string{weather} }}152 numbers := func() string {153 var got []string154 for _, e := range l.Entries() {155 got = append(got, e.Channel.Number()+" "+e.Channel.Name())156 }157 return strings.Join(got, "|")158 }159 if got := numbers(); got != "1.2 Fish|1.3 1000.1 Too Big|1.4 Birds" {160 t.Errorf("first: %s", got)161 }162 // The weather channel moving is noticed at once, not at the next scan.163 weather = "1.2"164 if got := numbers(); got != "1.1 1.2 Fish|1.3 1000.1 Too Big|1.4 Birds" {165 t.Errorf("after the weather moved: %s", got)166 }167}168169func TestServeLogo(t *testing.T) {170 path := filepath.Join(t.TempDir(), "logo.svg")171 writeFile(t, path, `<svg xmlns="http://www.w3.org/2000/svg"/>`)172 tag := LogoTag(path)173 if tag == "" || LogoTag(path+".gone") != "" {174 t.Fatalf("tags: %q", tag)175 }176 get := func(query string, header http.Header) *httptest.ResponseRecorder {177 r := httptest.NewRequest(http.MethodGet, "/channel-logos/1.4"+query, nil)178 for k, v := range header {179 r.Header[k] = v180 }181 w := httptest.NewRecorder()182 ServeLogo(w, r, path)183 return w184 }185 w := get("", nil)186 if w.Code != 200 || w.Header().Get("Content-Type") != "image/svg+xml" || w.Header().Get("ETag") != `"`+tag+`"` ||187 w.Header().Get("Cache-Control") != "public, max-age=300" || !strings.Contains(w.Header().Get("Content-Security-Policy"), "sandbox") ||188 !strings.HasPrefix(w.Body.String(), "<svg") {189 t.Errorf("logo: %d %v %q", w.Code, w.Header(), w.Body.String())190 }191 if w := get("?v="+tag, nil); w.Header().Get("Cache-Control") != "public, max-age=31536000, immutable" {192 t.Errorf("versioned: %v", w.Header())193 }194 if w := get("", http.Header{"If-None-Match": {`"` + tag + `"`}}); w.Code != http.StatusNotModified {195 t.Errorf("revalidated: %d", w.Code)196 }197 r := httptest.NewRequest(http.MethodGet, "/x", nil)198 w = httptest.NewRecorder()199 ServeLogo(w, r, path+".gone")200 if w.Code != http.StatusNotFound {201 t.Errorf("missing: %d", w.Code)202 }203}