package discovery import ( "slices" "testing" "github.com/mithral/vellum/internal/core" ) // baseConfig is a config with discovery on and a port that parses. func baseConfig() core.Config { return core.Config{Discovery: true, Listen: ":7788"} } func TestDefinitionsAdvertisesThreeServiceTypes(t *testing.T) { defs, err := Definitions(baseConfig(), "1.2.3", []string{"music", "ebooks"}) if err != nil { t.Fatalf("Definitions: %v", err) } want := []string{"_vellum._tcp", "_jellyfin._tcp", "_subsonic._tcp"} got := make([]string, 0, len(defs)) for _, d := range defs { got = append(got, d.Service) if d.Instance != "Vellum" { t.Errorf("%s Instance = %q, want Vellum", d.Service, d.Instance) } } if !slices.Equal(got, want) { t.Errorf("service types = %v, want %v", got, want) } } func TestDefinitionsTXTRecords(t *testing.T) { cfg := baseConfig() cfg.PublicURL = "https://media.example.com" defs, err := Definitions(cfg, "1.2.3", []string{"music", "ebooks"}) if err != nil { t.Fatalf("Definitions: %v", err) } want := map[string]string{ "api": "v1", "ver": "1.2.3", "proto": "vellum,jellyfin,subsonic", "types": "music,ebooks", "tls": "true", "pub": "https://media.example.com", } // Every record carries the same TXT: a client resolves one service type and // then needs to know what else this server answers. for _, d := range defs { if len(d.TXT) != len(want) { t.Fatalf("%s TXT = %v, want %d keys", d.Service, d.TXT, len(want)) } for k, v := range want { if d.TXT[k] != v { t.Errorf("%s TXT[%q] = %q, want %q", d.Service, k, d.TXT[k], v) } } } } func TestDefinitionsOmitsPubWithoutPublicURL(t *testing.T) { defs, err := Definitions(baseConfig(), "dev", nil) if err != nil { t.Fatalf("Definitions: %v", err) } d := defs[0] if _, ok := d.TXT["pub"]; ok { t.Errorf("TXT has pub=%q, want it absent when VELLUM_PUBLIC_URL is unset", d.TXT["pub"]) } if d.TXT["tls"] != "false" { t.Errorf("tls = %q, want false with no https origin", d.TXT["tls"]) } if d.TXT["types"] != "" { t.Errorf("types = %q, want empty with no media kinds", d.TXT["types"]) } } func TestDefinitionsUsesListenPort(t *testing.T) { cfg := baseConfig() cfg.Listen = "127.0.0.1:9999" defs, err := Definitions(cfg, "dev", nil) if err != nil { t.Fatalf("Definitions: %v", err) } for _, d := range defs { if d.Port != 9999 { t.Errorf("%s Port = %d, want 9999", d.Service, d.Port) } } } func TestDefinitionsDisabled(t *testing.T) { cfg := baseConfig() cfg.Discovery = false defs, err := Definitions(cfg, "dev", []string{"music"}) if err != nil { t.Fatalf("Definitions: %v", err) } if defs != nil { t.Errorf("Definitions = %v, want nil when VELLUM_DISCOVERY is off", defs) } } func TestDefinitionsSkipsLoopbackListen(t *testing.T) { // Nothing on the LAN can reach a loopback bind, which is the normal shape // behind a reverse proxy. Advertising it would put a port in the browse // list that refuses every client that dials it. for _, listen := range []string{"127.0.0.1:7788", "[::1]:7788"} { cfg := baseConfig() cfg.Listen = listen defs, err := Definitions(cfg, "dev", []string{"music"}) if err != nil { t.Fatalf("Definitions(%s): %v", listen, err) } if defs != nil { t.Errorf("Definitions(%s) = %v, want nil: loopback is unreachable from the LAN", listen, defs) } } // The other side of the boundary: a wildcard bind is reachable, so it must // still advertise. for _, listen := range []string{":7788", "0.0.0.0:7788"} { cfg := baseConfig() cfg.Listen = listen defs, err := Definitions(cfg, "dev", []string{"music"}) if err != nil { t.Fatalf("Definitions(%s): %v", listen, err) } if len(defs) == 0 { t.Errorf("Definitions(%s) advertised nothing; a wildcard bind is reachable", listen) } } } func TestDefinitionsRejectsListenWithoutPort(t *testing.T) { cfg := baseConfig() cfg.Listen = "7788" // no colon: not a host:port pair // Advertising port 0 produces a record no client can connect to, so this is // an error rather than a silently empty advertisement. if _, err := Definitions(cfg, "dev", nil); err == nil { t.Fatal("expected an error for a listen address with no port") } cfg.Listen = ":0" // kernel-assigned port, unknowable at registration time if _, err := Definitions(cfg, "dev", nil); err == nil { t.Fatal("expected an error for a kernel-assigned listen port") } } func TestTXTRecordsAreSorted(t *testing.T) { got := txtRecords(map[string]string{"ver": "1", "api": "v1", "tls": "false"}) want := []string{"api=v1", "tls=false", "ver=1"} if !slices.Equal(got, want) { t.Errorf("txtRecords = %v, want %v", got, want) } }