package arr import ( "encoding/json" "net/http" "net/http/httptest" "strings" "testing" ) func TestParseConfigXML(t *testing.T) { xml := ` 8989 abc123def456 /sonarr ` port, apiKey, urlBase := parseConfigXML(strings.NewReader(xml)) if port != "8989" { t.Errorf("port = %q, want 8989", port) } if apiKey != "abc123def456" { t.Errorf("apiKey = %q, want abc123def456", apiKey) } if urlBase != "/sonarr" { t.Errorf("urlBase = %q, want /sonarr", urlBase) } } func TestParseConfigXML_Minimal(t *testing.T) { xml := `7878key` port, apiKey, urlBase := parseConfigXML(strings.NewReader(xml)) if port != "7878" || apiKey != "key" || urlBase != "" { t.Errorf("got port=%q apiKey=%q urlBase=%q", port, apiKey, urlBase) } } func TestParseConfigXML_Invalid(t *testing.T) { port, apiKey, _ := parseConfigXML(strings.NewReader("not xml")) if port != "" || apiKey != "" { t.Errorf("invalid XML should return empty values") } } func TestExtractHostPort(t *testing.T) { tests := []struct { ports string container string want string }{ {"0.0.0.0:8989->8989/tcp", "8989", "8989"}, {"0.0.0.0:9090->8989/tcp, :::9090->8989/tcp", "8989", "9090"}, {"0.0.0.0:7878->7878/tcp", "7878", "7878"}, {"", "8989", ""}, {"0.0.0.0:3000->3000/tcp", "8989", ""}, } for _, tt := range tests { t.Run(tt.ports, func(t *testing.T) { got := extractHostPort(tt.ports, tt.container) if got != tt.want { t.Errorf("extractHostPort(%q, %q) = %q, want %q", tt.ports, tt.container, got, tt.want) } }) } } func TestDetectApp(t *testing.T) { tests := []struct { image string want string }{ {"linuxserver/sonarr:latest", "sonarr"}, {"hotio/radarr", "radarr"}, {"ghcr.io/linuxserver/prowlarr:develop", "prowlarr"}, {"nginx:latest", ""}, {"postgres:16", ""}, } for _, tt := range tests { t.Run(tt.image, func(t *testing.T) { got := detectApp(tt.image) if got != tt.want { t.Errorf("detectApp(%q) = %q, want %q", tt.image, got, tt.want) } }) } } func TestConfigDirs(t *testing.T) { dirs := configDirs() if len(dirs) == 0 { t.Error("configDirs() returned empty") } } func TestParseConfigXMLEmpty(t *testing.T) { port, apiKey, urlBase := parseConfigXML(strings.NewReader("")) if port != "" || apiKey != "" || urlBase != "" { t.Error("empty input should return empty values") } } func TestParseConfigXMLNoPort(t *testing.T) { xml := `key123` port, apiKey, _ := parseConfigXML(strings.NewReader(xml)) if port != "" { t.Errorf("port = %q, want empty", port) } if apiKey != "key123" { t.Errorf("apiKey = %q, want key123", apiKey) } } func TestExtractHostPortMultipleMappings(t *testing.T) { tests := []struct { name string ports string container string want string }{ {"ipv6 only", ":::8989->8989/tcp", "8989", "8989"}, {"different host port", "0.0.0.0:9999->8989/tcp", "8989", "9999"}, {"port in string but no mapping", "something 8989 somewhere", "8989", "8989"}, {"no match at all", "0.0.0.0:3000->3000/tcp", "9999", ""}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { got := extractHostPort(tt.ports, tt.container) if got != tt.want { t.Errorf("extractHostPort(%q, %q) = %q, want %q", tt.ports, tt.container, got, tt.want) } }) } } func TestDiscoverFromProwlarr(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { case "/api/v1/applications": json.NewEncoder(w).Encode([]Application{ { ID: 1, Name: "Radarr", Fields: []Field{ {Name: "baseUrl", Value: "http://localhost:7878"}, {Name: "apiKey", Value: "radarr-key-123"}, }, }, { ID: 2, Name: "Sonarr", Fields: []Field{ {Name: "baseUrl", Value: "http://localhost:8989"}, {Name: "apiKey", Value: "sonarr-key-456"}, }, }, { ID: 3, Name: "Unknown App", Fields: []Field{ {Name: "baseUrl", Value: "http://localhost:9000"}, {Name: "apiKey", Value: "unknown-key"}, }, }, { ID: 4, Name: "Incomplete", Fields: []Field{ {Name: "baseUrl", Value: "http://localhost:5000"}, // no apiKey → should be skipped }, }, }) case "/api/v3/system/status": json.NewEncoder(w).Encode(SystemStatus{AppName: "Radarr", Version: "4.0.0"}) default: w.WriteHeader(http.StatusNotFound) } })) defer srv.Close() // DiscoverFromProwlarr will try to verify each instance, which will fail // for localhost URLs (not our test server), but that's OK — we test the parsing instances := DiscoverFromProwlarr(srv.URL, "prowlarr-key") // Should find Radarr and Sonarr (Unknown and Incomplete skipped) if len(instances) != 2 { t.Fatalf("expected 2 instances, got %d: %+v", len(instances), instances) } found := map[string]bool{} for _, inst := range instances { found[inst.App] = true if inst.Source != "prowlarr" { t.Errorf("source = %q, want prowlarr", inst.Source) } } if !found["radarr"] { t.Error("expected radarr instance") } if !found["sonarr"] { t.Error("expected sonarr instance") } } func TestVerify(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if r.Header.Get("X-Api-Key") != "valid-key" { w.WriteHeader(http.StatusUnauthorized) return } json.NewEncoder(w).Encode(SystemStatus{AppName: "Radarr", Version: "5.0.0"}) })) defer srv.Close() t.Run("valid", func(t *testing.T) { inst := &Instance{App: "radarr", URL: srv.URL, APIKey: "valid-key"} err := Verify(inst) if err != nil { t.Fatalf("Verify: %v", err) } if inst.Version != "5.0.0" { t.Errorf("version = %q, want 5.0.0", inst.Version) } }) t.Run("no api key", func(t *testing.T) { inst := &Instance{App: "radarr", URL: srv.URL} err := Verify(inst) if err == nil { t.Error("expected error for no API key") } }) t.Run("invalid key", func(t *testing.T) { inst := &Instance{App: "radarr", URL: srv.URL, APIKey: "wrong-key"} err := Verify(inst) if err == nil { t.Error("expected error for invalid API key") } }) }