package coverart import ( "context" "errors" "net/http" "net/http/httptest" "sync/atomic" "testing" "time" ) // newDeezerTestProvider returns a fresh enabled provider configured // against a test server URL. func newDeezerTestProvider(t *testing.T, baseURL string) *deezerProvider { t.Helper() old := deezerBaseURL deezerBaseURL = baseURL t.Cleanup(func() { deezerBaseURL = old }) p := &deezerProvider{client: newHTTPClient(httpClientOptions{Name: "deezer", HTTPClient: &http.Client{Timeout: 5 * time.Second}, BaseBackoff: time.Millisecond})} p.enabled.Store(true) return p } func TestDeezer_ID(t *testing.T) { p := &deezerProvider{} if p.ID() != "deezer" { t.Errorf("ID = %q, want deezer", p.ID()) } if p.RequiresAPIKey() { t.Error("RequiresAPIKey = true, want false") } if !p.DefaultEnabled() { t.Error("DefaultEnabled = false, want true") } } func TestDeezer_FetchArtistArt_Success(t *testing.T) { mux := http.NewServeMux() srv := httptest.NewServer(mux) defer srv.Close() mux.HandleFunc("/search/artist", func(w http.ResponseWriter, r *http.Request) { imgURL := "http://" + r.Host + "/img/cp.jpg" w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[{"name":"Caravan Palace","picture_xl":"` + imgURL + `"}]}`)) }) mux.HandleFunc("/img/", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "image/jpeg") _, _ = w.Write([]byte("img-bytes")) }) p := newDeezerTestProvider(t, srv.URL) thumb, fanart, err := p.FetchArtistArt(context.Background(), ArtistRef{Name: "Caravan Palace"}) if err != nil { t.Fatalf("err = %v, want nil", err) } if string(thumb) != "img-bytes" { t.Errorf("thumb = %q, want img-bytes", thumb) } if fanart != nil { t.Errorf("fanart = %v, want nil (Deezer has no separate fanart)", fanart) } } func TestDeezer_FetchArtistArt_EmptyResults(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[],"total":0}`)) })) defer srv.Close() p := newDeezerTestProvider(t, srv.URL) _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{Name: "Daisy Grenade"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound", err) } } func TestDeezer_FetchArtistArt_NameMismatch(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[{"name":"Some Other Artist","picture_xl":"http://example/x.jpg"}]}`)) })) defer srv.Close() p := newDeezerTestProvider(t, srv.URL) _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{Name: "Real Artist"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound (name mismatch should not match)", err) } } func TestDeezer_FetchArtistArt_EmptyName(t *testing.T) { p := &deezerProvider{client: newHTTPClient(httpClientOptions{Name: "deezer", HTTPClient: &http.Client{Timeout: 5 * time.Second}, BaseBackoff: time.Millisecond})} p.enabled.Store(true) _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound on empty Name", err) } } func TestDeezer_FetchArtistArt_Disabled(t *testing.T) { p := &deezerProvider{client: newHTTPClient(httpClientOptions{Name: "deezer", HTTPClient: &http.Client{Timeout: 5 * time.Second}, BaseBackoff: time.Millisecond})} // enabled defaults to false _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{Name: "X"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound when disabled", err) } } func TestDeezer_FetchAlbumCover_Success(t *testing.T) { mux := http.NewServeMux() srv := httptest.NewServer(mux) defer srv.Close() mux.HandleFunc("/search/album", func(w http.ResponseWriter, r *http.Request) { imgURL := "http://" + r.Host + "/img/r.jpg" w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[{"title":"The Record","cover_xl":"` + imgURL + `","artist":{"name":"The Band"}}]}`)) }) mux.HandleFunc("/img/", func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "image/jpeg") _, _ = w.Write([]byte("album-bytes")) }) p := newDeezerTestProvider(t, srv.URL) body, err := p.FetchAlbumCover(context.Background(), AlbumRef{ArtistName: "The Band", AlbumTitle: "The Record"}) if err != nil { t.Fatalf("err = %v, want nil", err) } if string(body) != "album-bytes" { t.Errorf("body = %q, want album-bytes", body) } } func TestDeezer_FetchAlbumCover_TitleMismatch(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[{"title":"Wrong Album","cover_xl":"http://example/x.jpg","artist":{"name":"The Band"}}]}`)) })) defer srv.Close() p := newDeezerTestProvider(t, srv.URL) _, err := p.FetchAlbumCover(context.Background(), AlbumRef{ArtistName: "The Band", AlbumTitle: "The Record"}) if !errors.Is(err, ErrNotFound) { t.Errorf("err = %v, want ErrNotFound (title mismatch)", err) } } func TestDeezer_FetchAlbumCover_MissingFields(t *testing.T) { p := &deezerProvider{client: newHTTPClient(httpClientOptions{Name: "deezer", HTTPClient: &http.Client{Timeout: 5 * time.Second}, BaseBackoff: time.Millisecond})} p.enabled.Store(true) cases := []AlbumRef{ {}, {ArtistName: "X"}, // no title {AlbumTitle: "Y"}, // no artist } for _, ref := range cases { _, err := p.FetchAlbumCover(context.Background(), ref) if !errors.Is(err, ErrNotFound) { t.Errorf("ref=%+v err=%v, want ErrNotFound", ref, err) } } } func TestDeezer_RateLimit_Serializes(t *testing.T) { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { w.Header().Set("Content-Type", "application/json") _, _ = w.Write([]byte(`{"data":[]}`)) })) defer srv.Close() old := deezerBaseURL deezerBaseURL = srv.URL t.Cleanup(func() { deezerBaseURL = old }) // Build a provider WITH the production MinInterval so this test can // verify the rate-limit actually serializes. Other tests use // newDeezerTestProvider which leaves MinInterval=0 for speed. p := &deezerProvider{client: newHTTPClient(httpClientOptions{ Name: "deezer", HTTPClient: &http.Client{Timeout: 5 * time.Second}, MinInterval: deezerMinPeriod, BaseBackoff: time.Millisecond, })} p.enabled.Store(true) start := time.Now() _, _, _ = p.FetchArtistArt(context.Background(), ArtistRef{Name: "A"}) _, _, _ = p.FetchArtistArt(context.Background(), ArtistRef{Name: "B"}) elapsed := time.Since(start) if elapsed < deezerMinPeriod { t.Errorf("two back-to-back calls completed in %v, want at least %v (rate-limit didn't serialize)", elapsed, deezerMinPeriod) } } func TestDeezer_FetchArtistArt_ServerError(t *testing.T) { var attempts atomic.Int32 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { attempts.Add(1) w.WriteHeader(http.StatusInternalServerError) })) defer srv.Close() p := newDeezerTestProvider(t, srv.URL) _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{Name: "X"}) if !errors.Is(err, ErrTransient) { t.Errorf("err = %v, want ErrTransient on 500", err) } // Should have retried up to deezerMaxRetries+1 times total. if attempts.Load() < 2 { t.Errorf("attempts = %d, want >= 2 (retry should have run)", attempts.Load()) } } func TestDeezer_FetchArtistArt_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(`{"data":[]}`)) // second attempt: empty → ErrNotFound })) defer srv.Close() p := newDeezerTestProvider(t, srv.URL) _, _, err := p.FetchArtistArt(context.Background(), ArtistRef{Name: "X"}) if attempts.Load() < 2 { t.Errorf("attempts = %d, want >= 2 (retry should have run)", attempts.Load()) } if !errors.Is(err, ErrNotFound) { t.Errorf("final err = %v, want ErrNotFound (empty results after retry)", err) } }