package musicbrainz import ( "context" "net/http" "net/http/httptest" "testing" "golang.org/x/time/rate" "naviwatcher/internal/config" ) // ---------- FilterReleaseGroups tests ---------- func TestFilterReleaseGroups_ExcludesBootlegPromotionPseudo(t *testing.T) { groups := []ReleaseGroup{ {ID: "rg-1", Title: "Official Album", Type: "Album", Status: "Official"}, {ID: "rg-2", Title: "Bootleg Live", Type: "Album", Status: "Bootleg"}, {ID: "rg-3", Title: "Promo CD", Type: "Single", Status: "Promotion"}, {ID: "rg-4", Title: "Pseudo Release", Type: "Album", Status: "Pseudo-Release"}, } result := FilterReleaseGroups(groups, FilterOptions{}) if len(result) != 1 { t.Fatalf("FilterReleaseGroups() returned %d groups, want 1", len(result)) } if result[0].ID != "rg-1" { t.Errorf("FilterReleaseGroups()[0].ID = %q, want %q", result[0].ID, "rg-1") } } func TestFilterReleaseGroups_IncludesOnlyAllowedTypes(t *testing.T) { groups := []ReleaseGroup{ {ID: "rg-1", Title: "Album", Type: "Album", Status: "Official"}, {ID: "rg-2", Title: "Single", Type: "Single", Status: "Official"}, {ID: "rg-3", Title: "EP", Type: "EP", Status: "Official"}, {ID: "rg-4", Title: "Compilation", Type: "Compilation", Status: "Official"}, {ID: "rg-5", Title: "Soundtrack", Type: "Soundtrack", Status: "Official"}, {ID: "rg-6", Title: "Live", Type: "Live", Status: "Official"}, {ID: "rg-7", Title: "Remix", Type: "Remix", Status: "Official"}, } result := FilterReleaseGroups(groups, FilterOptions{}) if len(result) != 4 { t.Fatalf("FilterReleaseGroups() returned %d groups, want 4", len(result)) } allowedIDs := map[string]bool{"rg-1": true, "rg-2": true, "rg-3": true, "rg-4": true} for _, rg := range result { if !allowedIDs[rg.ID] { t.Errorf("unexpected group %q in filtered results", rg.ID) } } } func TestFilterReleaseGroups_IgnoreSingles(t *testing.T) { groups := []ReleaseGroup{ {ID: "rg-1", Title: "Album", Type: "Album", Status: "Official"}, {ID: "rg-2", Title: "Single", Type: "Single", Status: "Official"}, {ID: "rg-3", Title: "EP", Type: "EP", Status: "Official"}, } result := FilterReleaseGroups(groups, FilterOptions{IgnoreSingles: true}) if len(result) != 2 { t.Fatalf("FilterReleaseGroups() returned %d groups, want 2", len(result)) } for _, rg := range result { if rg.Type == "Single" { t.Errorf("single %q should have been filtered out", rg.ID) } } } func TestFilterReleaseGroups_IgnoreCompilations(t *testing.T) { groups := []ReleaseGroup{ {ID: "rg-1", Title: "Album", Type: "Album", Status: "Official"}, {ID: "rg-2", Title: "Compilation", Type: "Compilation", Status: "Official"}, {ID: "rg-3", Title: "EP", Type: "EP", Status: "Official"}, } result := FilterReleaseGroups(groups, FilterOptions{IgnoreCompilations: true}) if len(result) != 2 { t.Fatalf("FilterReleaseGroups() returned %d groups, want 2", len(result)) } for _, rg := range result { if rg.Type == "Compilation" { t.Errorf("compilation %q should have been filtered out", rg.ID) } } } // ---------- ReleaseGroup.ToExternalRelease tests ---------- func TestReleaseGroup_ToExternalRelease(t *testing.T) { rg := ReleaseGroup{ ID: "rg-uuid-1", Title: "Dark Side of the Moon", Type: "Album", Status: "Official", ArtistID: "mbid-artist-uuid-1", ArtistName: "Pink Floyd", ReleaseDate: "1973-03-01", } // ToExternalRelease stores the canonical artist key (Navidrome ID), not the // MusicBrainz ArtistID, so external_releases.artist_id matches artist_settings. const navidromeArtistID = "navidrome-artist-uuid-1" er := rg.ToExternalRelease(navidromeArtistID) if er.RGID != "rg-uuid-1" { t.Errorf("RGID = %q, want %q", er.RGID, "rg-uuid-1") } if er.ArtistID != navidromeArtistID { t.Errorf("ArtistID = %q, want %q", er.ArtistID, navidromeArtistID) } if er.Title != "Dark Side of the Moon" { t.Errorf("Title = %q, want %q", er.Title, "Dark Side of the Moon") } if er.Type != "Album" { t.Errorf("Type = %q, want %q", er.Type, "Album") } if er.ReleaseDate != "1973-03-01" { t.Errorf("ReleaseDate = %q, want %q", er.ReleaseDate, "1973-03-01") } } // ---------- GetArtistReleaseGroups tests ---------- func TestGetArtistReleaseGroups_Success(t *testing.T) { artistMBID := "artist-uuid-test" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/xml") w.Write([]byte(` Dark Side of the Moon 1973-03-01 Pink Floyd Another Brick in the Wall 1979-11-30 Pink Floyd `)) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } groups, err := client.GetArtistReleaseGroups(context.Background(), artistMBID) if err != nil { t.Fatalf("GetArtistReleaseGroups() error = %v", err) } if len(groups) != 2 { t.Fatalf("GetArtistReleaseGroups() returned %d groups, want 2", len(groups)) } if groups[0].Title != "Dark Side of the Moon" { t.Errorf("groups[0].Title = %q, want %q", groups[0].Title, "Dark Side of the Moon") } if groups[1].Title != "Another Brick in the Wall" { t.Errorf("groups[1].Title = %q, want %q", groups[1].Title, "Another Brick in the Wall") } } func TestGetArtistReleaseGroups_Pagination(t *testing.T) { artistMBID := "artist-page-test" requestCount := 0 server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { requestCount++ offset := r.URL.Query().Get("offset") w.Header().Set("Content-Type", "application/xml") if offset == "0" || offset == "" { // First page: return 100 results (full page, matching limit) to trigger pagination xml := ` ` for i := 0; i < 100; i++ { xml += ` Page 1 Album 2020-01-01 Artist ` } xml += ` ` w.Write([]byte(xml)) } else { // Second page: return only 1 result (< limit, signaling last page) w.Write([]byte(` Page 2 Album 2021-01-01 Artist `)) } })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } groups, err := client.GetArtistReleaseGroups(context.Background(), artistMBID) if err != nil { t.Fatalf("GetArtistReleaseGroups() error = %v", err) } // Should have fetched 2 pages: 100 from first + 1 from second = 101 total if len(groups) != 101 { t.Fatalf("GetArtistReleaseGroups() returned %d groups, want 101", len(groups)) } if requestCount != 2 { t.Errorf("expected 2 paginated requests, got %d", requestCount) } } func TestGetArtistReleaseGroups_EmptyResult(t *testing.T) { artistMBID := "artist-empty" server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/xml") w.Write([]byte(` `)) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } groups, err := client.GetArtistReleaseGroups(context.Background(), artistMBID) if err != nil { t.Fatalf("GetArtistReleaseGroups() error = %v", err) } if len(groups) != 0 { t.Errorf("GetArtistReleaseGroups() returned %d groups, want 0", len(groups)) } } func TestGetArtistReleaseGroups_ServerError(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusServiceUnavailable) w.Write([]byte("Rate limit exceeded")) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } _, err := client.GetArtistReleaseGroups(context.Background(), "artist-1") if err == nil { t.Fatal("GetArtistReleaseGroups() expected error for server error, got nil") } } func TestGetArtistReleaseGroups_InvalidXML(t *testing.T) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Content-Type", "application/xml") w.Write([]byte(`this is not valid xml`)) })) defer server.Close() cfg := config.MusicBrainzConfig{ UserAgent: "naviwatcher/0.1.0 (test@example.com)", } rl := rate.NewLimiter(rate.Limit(1000), 1000) client := &MusicBrainzClient{ httpClient: server.Client(), userAgent: cfg.UserAgent, baseURL: server.URL, rateLimiter: rl, } _, err := client.GetArtistReleaseGroups(context.Background(), "artist-1") if err == nil { t.Fatal("GetArtistReleaseGroups() expected error for invalid XML, got nil") } }