package coverart import ( "context" "encoding/json" "errors" "net/http" "net/http/httptest" "strings" "sync/atomic" "testing" "time" ) // withTheAudioDBBaseURL temporarily redirects theAudioDBBaseURL to a // test server, returning a function the caller defers to restore. func withTheAudioDBBaseURL(t *testing.T, mockURL string) { t.Helper() old := theAudioDBBaseURL theAudioDBBaseURL = mockURL t.Cleanup(func() { theAudioDBBaseURL = old }) } func newTheAudioDBProviderForTest(t *testing.T, mockURL string) *theAudioDBProvider { t.Helper() resetRegistryForTests() t.Cleanup(resetRegistryForTests) withTheAudioDBBaseURL(t, mockURL) p := &theAudioDBProvider{ client: newHTTPClient(httpClientOptions{Name: "theaudiodb", HTTPClient: &http.Client{Timeout: 5 * time.Second}, TreatAuthAsTransient: true, BaseBackoff: time.Millisecond}), } Register(p) p.enabled.Store(true) key := theAudioDBTestKey p.apiKey.Store(&key) return p } func TestTheAudioDB_ID(t *testing.T) { resetRegistryForTests() defer resetRegistryForTests() p := &theAudioDBProvider{} if p.ID() != "theaudiodb" { t.Errorf("ID = %q, want theaudiodb", p.ID()) } if !p.RequiresAPIKey() { t.Error("RequiresAPIKey = false, want true") } if !p.DefaultEnabled() { t.Error("DefaultEnabled = false, want true") } } func TestTheAudioDB_FetchAlbumCover_Success(t *testing.T) { imageBody := []byte("FAKE_JPEG") srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/album-mb.php"): w.Header().Set("Content-Type", "application/json") // Use the test server's own URL for the image, so getImage // hits the same server. imageURL := "http://" + r.Host + "/img/album.jpg" _ = json.NewEncoder(w).Encode(map[string]any{ "album": []map[string]any{ {"strAlbumThumb": imageURL}, }, }) case r.URL.Path == "/img/album.jpg": w.Header().Set("Content-Type", "image/jpeg") _, _ = w.Write(imageBody) default: http.Error(w, "unexpected path: "+r.URL.Path, http.StatusBadRequest) } })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) body, err := p.FetchAlbumCover(context.Background(), AlbumRef{MBID: "test-mbid"}) if err != nil { t.Fatalf("FetchAlbumCover err = %v, want nil", err) } if string(body) != string(imageBody) { t.Errorf("body = %q, want %q", body, imageBody) } } func TestTheAudioDB_FetchAlbumCover_NotFound(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"album":null}`)) })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) _, err := p.FetchAlbumCover(context.Background(), AlbumRef{MBID: "missing-mbid"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } } func TestTheAudioDB_FetchAlbumCover_NullThumbURL(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"album":[{"strAlbumThumb":null}]}`)) })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) _, err := p.FetchAlbumCover(context.Background(), AlbumRef{MBID: "mbid"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound when thumb URL is null", err) } } func TestTheAudioDB_FetchAlbumCover_429TriggersRetry(t *testing.T) { var attempts atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { n := attempts.Add(1) if n == 1 { http.Error(w, "rate limit", http.StatusTooManyRequests) return } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"album":null}`)) // second attempt: empty (still ErrNotFound, but past the retry) })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) _, err := p.FetchAlbumCover(context.Background(), AlbumRef{MBID: "mbid"}) if attempts.Load() < 2 { t.Errorf("attempts = %d, want >= 2 (retry should have run)", attempts.Load()) } // Final result is ErrNotFound (empty album response on retry). if !errors.Is(err, ErrNotFound) { t.Errorf("final err = %v, want ErrNotFound", err) } } func TestTheAudioDB_FetchArtistArt_BothPresent(t *testing.T) { thumbBody := []byte("THUMB") fanartBody := []byte("FANART") srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/artist-mb.php"): thumbURL := "http://" + r.Host + "/img/thumb.jpg" fanartURL := "http://" + r.Host + "/img/fanart.jpg" w.Header().Set("Content-Type", "application/json") _ = json.NewEncoder(w).Encode(map[string]any{ "artists": []map[string]any{ {"strArtistThumb": thumbURL, "strArtistFanart": fanartURL}, }, }) case r.URL.Path == "/img/thumb.jpg": w.Header().Set("Content-Type", "image/jpeg") _, _ = w.Write(thumbBody) case r.URL.Path == "/img/fanart.jpg": w.Header().Set("Content-Type", "image/jpeg") _, _ = w.Write(fanartBody) default: http.Error(w, "unexpected: "+r.URL.Path, http.StatusBadRequest) } })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) thumb, fanart, err := p.FetchArtistArt(context.Background(), ArtistRef{MBID: "mbid"}) if err != nil { t.Fatalf("err = %v", err) } if string(thumb) != "THUMB" { t.Errorf("thumb = %q, want THUMB", thumb) } if string(fanart) != "FANART" { t.Errorf("fanart = %q, want FANART", fanart) } } func TestTheAudioDB_FetchArtistArt_ThumbOnly(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { switch { case strings.Contains(r.URL.Path, "/artist-mb.php"): thumbURL := "http://" + r.Host + "/img/thumb.jpg" w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"artists":[{"strArtistThumb":"` + thumbURL + `","strArtistFanart":null}]}`)) case r.URL.Path == "/img/thumb.jpg": w.Header().Set("Content-Type", "image/jpeg") _, _ = w.Write([]byte("T")) default: http.Error(w, "unexpected: "+r.URL.Path, http.StatusBadRequest) } })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) thumb, fanart, err := p.FetchArtistArt(context.Background(), ArtistRef{MBID: "mbid"}) if err != nil { t.Fatalf("err = %v", err) } if string(thumb) != "T" { t.Errorf("thumb = %q, want T", thumb) } if fanart != nil { t.Errorf("fanart = %v, want nil", fanart) } } func TestTheAudioDB_FetchArtistArt_BothEmpty(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"artists":[{"strArtistThumb":null,"strArtistFanart":null}]}`)) })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{MBID: "mbid"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } } func TestTheAudioDB_DefaultsToTestKeyOnEmpty(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // Inspect the URL — should contain "/2/" (the test key). if !strings.Contains(r.URL.Path, "/"+theAudioDBTestKey+"/") { t.Errorf("URL path %q missing test key", r.URL.Path) } w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"album":null}`)) })) defer srv.Close() resetRegistryForTests() defer resetRegistryForTests() withTheAudioDBBaseURL(t, srv.URL) p := &theAudioDBProvider{client: newHTTPClient(httpClientOptions{Name: "theaudiodb", HTTPClient: &http.Client{Timeout: 5 * time.Second}, TreatAuthAsTransient: true, BaseBackoff: time.Millisecond})} Register(p) // Configure with empty key — should fall back to test key. if err := p.Configure(ProviderSettings{Enabled: true, APIKey: ""}); err != nil { t.Fatalf("Configure: %v", err) } _, _ = p.FetchAlbumCover(context.Background(), AlbumRef{MBID: "mbid"}) } func TestTheAudioDB_DisabledReturnsNotFound(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { t.Error("server should not be hit when provider is disabled") })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) p.enabled.Store(false) _, err := p.FetchAlbumCover(context.Background(), AlbumRef{MBID: "any"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound (disabled)", err) } _, _, err = p.FetchArtistArt(context.Background(), ArtistRef{MBID: "any"}) if !errors.Is(err, ErrNotFound) { t.Errorf("artist err = %v, want ErrNotFound (disabled)", err) } } func TestTheAudioDB_TestConnection_SuccessOnEmptyAlbum(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"album":null}`)) })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) if err := p.TestConnection(context.Background()); err != nil { t.Errorf("TestConnection err = %v, want nil (empty album response is still 200)", err) } } func TestTheAudioDB_TestConnection_FailureOn401(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { http.Error(w, "unauthorized", http.StatusUnauthorized) })) defer srv.Close() p := newTheAudioDBProviderForTest(t, srv.URL) if err := p.TestConnection(context.Background()); err == nil { t.Error("TestConnection err = nil, want non-nil for 401") } }