1package hdhr23import (4 "bytes"5 "context"6 "encoding/binary"7 "encoding/json"8 "fmt"9 "io"10 "net/http"11 "net/http/httptest"12 "strings"13 "testing"14)1516func TestDeviceIDCheckDigit(t *testing.T) {17 for _, seed := range []string{"nas", "airwaves", "x"} {18 id := DeviceIDFor(seed)19 if !ValidDeviceID(id) {20 t.Errorf("DeviceIDFor(%q) = %08X fails the check digit", seed, id)21 }22 if ValidDeviceID(id ^ 1) {23 t.Errorf("%08X with a flipped bit still validates", id^1)24 }25 }26}2728func discoverRequest(deviceType, id uint32) []byte {29 var b bytes.Buffer30 tlv(&b, tagDeviceType, u32(deviceType))31 tlv(&b, tagDeviceID, u32(id))32 return packet(typeDiscoverRq, b.Bytes())33}3435func TestDiscoveryRequestAndReply(t *testing.T) {36 id := DeviceIDFor("nas")37 if !parseRequest(discoverRequest(wildcard, wildcard), id) {38 t.Error("wildcard search not answered")39 }40 if !parseRequest(discoverRequest(deviceTuner, id), id) {41 t.Error("search for our ID not answered")42 }43 if parseRequest(discoverRequest(deviceTuner, id^0x10), id) {44 t.Error("search for another device answered")45 }46 if parseRequest(discoverRequest(5, wildcard), id) {47 t.Error("search for another device type answered")48 }4950 r := reply(id, 2, "http://192.168.1.20:5004")51 n := int(binary.BigEndian.Uint16(r[2:]))52 if binary.BigEndian.Uint16(r) != typeDiscoverRp || !bytes.Contains(r[4:4+n], []byte("http://192.168.1.20:5004/lineup.json")) {53 t.Errorf("reply % x", r)54 }55}5657type fakeBackend struct{}5859func (fakeBackend) Lineup(context.Context) ([]Entry, error) {60 return []Entry{{Number: "7.1", Name: "KMGH", HD: true, Group: "Antenna"}, {Number: "1.1", Name: "Airwaves Weather", Group: "Airwaves", Logo: "/channel-logos/1.1"}}, nil61}6263func (fakeBackend) Stream(_ context.Context, number string, w io.Writer) error {64 _, err := fmt.Fprintf(w, "TS:%s", number)65 return err66}6768func (fakeBackend) XMLTV(_ context.Context, w io.Writer, url func(string) string) error {69 _, err := fmt.Fprintf(w, "<tv>%s</tv>", url("/auto/v7.1"))70 return err71}7273func TestHTTPAPI(t *testing.T) {74 logos := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, "logo "+r.URL.Path) })75 s := &Server{Backend: fakeBackend{}, DeviceID: DeviceIDFor("nas"), FriendlyName: "Airwaves", Tuners: func() int { return 2 }, Logos: logos}76 srv := httptest.NewServer(s.Handler())77 defer srv.Close()78 get := func(path string) string {79 resp, err := http.Get(srv.URL + path)80 if err != nil {81 t.Fatal(err)82 }83 defer resp.Body.Close()84 b, _ := io.ReadAll(resp.Body)85 return string(b)86 }8788 var disc map[string]any89 if err := json.Unmarshal([]byte(get("/discover.json")), &disc); err != nil {90 t.Fatal(err)91 }92 if disc["LineupURL"] != srv.URL+"/lineup.json" || disc["TunerCount"].(float64) != 2 {93 t.Errorf("discover.json = %v", disc)94 }95 var lineup []map[string]any96 if err := json.Unmarshal([]byte(get("/lineup.json")), &lineup); err != nil {97 t.Fatal(err)98 }99 if len(lineup) != 2 || lineup[0]["URL"] != srv.URL+"/auto/v7.1" || lineup[0]["HD"].(float64) != 1 {100 t.Errorf("lineup.json = %v", lineup)101 }102 if got := get("/auto/v1.1"); got != "TS:1.1" {103 t.Errorf("stream = %q", got)104 }105 if got := get("/tuner1/v7.1"); got != "TS:7.1" {106 t.Errorf("tuner stream = %q", got)107 }108 m3u := get("/channels.m3u")109 if !strings.Contains(m3u, `tvg-chno="1.1"`) || !strings.Contains(m3u, srv.URL+"/auto/v1.1") || !strings.Contains(m3u, "/xmltv.xml") ||110 !strings.Contains(m3u, `tvg-logo="`+srv.URL+`/channel-logos/1.1"`) || !strings.Contains(m3u, `tvg-logo=""`) {111 t.Errorf("m3u:\n%s", m3u)112 }113 if got := get("/channel-logos/1.1"); got != "logo /channel-logos/1.1" {114 t.Errorf("logo = %q", got)115 }116 if got := get("/xmltv.xml"); got != "<tv>"+srv.URL+"/auto/v7.1</tv>" {117 t.Errorf("xmltv = %q", got)118 }119}120121// TestFromHost: without discovery, URLs use the address the client was122// given, as behind a container's port mapping, not the local address.123func TestFromHost(t *testing.T) {124 s := &Server{Backend: fakeBackend{}, DeviceID: DeviceIDFor("friend"), FriendlyName: "Airwaves", Tuners: func() int { return 2 }, FromHost: true}125 srv := httptest.NewServer(s.Handler())126 defer srv.Close()127 req, _ := http.NewRequest(http.MethodGet, srv.URL+"/lineup.json", nil)128 req.Host = "nas.local:15004"129 resp, err := http.DefaultClient.Do(req)130 if err != nil {131 t.Fatal(err)132 }133 defer resp.Body.Close()134 var lineup []map[string]any135 if err := json.NewDecoder(resp.Body).Decode(&lineup); err != nil {136 t.Fatal(err)137 }138 if len(lineup) != 2 || lineup[0]["URL"] != "http://nas.local:15004/auto/v7.1" {139 t.Errorf("lineup.json = %v", lineup)140 }141}