feat(server/m7-cover-sources): MBCAA provider wrapper
Adapts the existing Fetcher-based MBCAA client to the new Provider abstraction. mbcaaProvider embeds *Fetcher and implements Provider + AlbumCoverProvider + TestableProvider. init() registers a default-config instance; NewMBCAAProviderFromConfig swaps in production config (User-Agent, MinPeriod) at boot. Wrapper approach (rather than reimplementing the HTTP logic) keeps the package compiling during the T4–T8 window: enricher.go and main.go still reference the legacy Fetcher type / NewFetcher constructor / Fetch method, all of which remain unchanged in fetcher.go. A-T8 will consolidate the HTTP logic into this file and delete fetcher.go after the enricher rewrite removes the last legacy call sites. TestConnection hits a hardcoded popular release MBID (Beatles "Abbey Road" UK release) and treats both 200 and 404 as "connection works" — MBCAA needs no auth, so only 5xx / network failures surface. Tests cover ID/DisplayName/Capability metadata, success path, 404→ErrNotFound, disabled-returns-ErrNotFound, Configure toggle, TestConnection variants, and NewMBCAAProviderFromConfig swap.
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
package coverart
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// newMBCAAProviderForTest registers a fresh mbcaaProvider pointing at
|
||||
// a test server, mirroring the pattern from fetcher_test.go but
|
||||
// exercising the interface-side methods.
|
||||
func newMBCAAProviderForTest(t *testing.T, mockURL string) *mbcaaProvider {
|
||||
t.Helper()
|
||||
resetRegistryForTests()
|
||||
t.Cleanup(resetRegistryForTests)
|
||||
|
||||
p := &mbcaaProvider{
|
||||
fetcher: NewFetcher(FetcherConfig{
|
||||
BaseURL: mockURL,
|
||||
UserAgent: "Minstrel/test (https://example.com)",
|
||||
MinPeriod: 0,
|
||||
HTTPClient: &http.Client{Timeout: 5 * time.Second},
|
||||
}),
|
||||
}
|
||||
Register(p)
|
||||
p.enabled.Store(true)
|
||||
return p
|
||||
}
|
||||
|
||||
func TestMBCAAProvider_ID(t *testing.T) {
|
||||
resetRegistryForTests()
|
||||
defer resetRegistryForTests()
|
||||
p := &mbcaaProvider{fetcher: NewFetcher(FetcherConfig{})}
|
||||
if p.ID() != "mbcaa" {
|
||||
t.Errorf("ID = %q, want mbcaa", p.ID())
|
||||
}
|
||||
if p.DisplayName() != "Cover Art Archive" {
|
||||
t.Errorf("DisplayName = %q, want Cover Art Archive", p.DisplayName())
|
||||
}
|
||||
if p.RequiresAPIKey() {
|
||||
t.Error("RequiresAPIKey = true, want false")
|
||||
}
|
||||
if !p.DefaultEnabled() {
|
||||
t.Error("DefaultEnabled = false, want true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMBCAAProvider_FetchAlbumCover_Success(t *testing.T) {
|
||||
imageBody := []byte("FAKE_JPEG")
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if !strings.Contains(r.URL.Path, "/release/abc-123/front-500") {
|
||||
http.Error(w, "wrong path: "+r.URL.Path, http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
w.Header().Set("Content-Type", "image/jpeg")
|
||||
_, _ = w.Write(imageBody)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := newMBCAAProviderForTest(t, srv.URL)
|
||||
body, err := p.FetchAlbumCover(context.Background(), "abc-123")
|
||||
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 TestMBCAAProvider_FetchAlbumCover_NotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := newMBCAAProviderForTest(t, srv.URL)
|
||||
_, err := p.FetchAlbumCover(context.Background(), "missing")
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("err = %v, want ErrNotFound", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMBCAAProvider_FetchAlbumCover_DisabledReturnsNotFound(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
t.Error("server should not be hit when provider is disabled")
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := newMBCAAProviderForTest(t, srv.URL)
|
||||
p.enabled.Store(false)
|
||||
|
||||
_, err := p.FetchAlbumCover(context.Background(), "any")
|
||||
if !errors.Is(err, ErrNotFound) {
|
||||
t.Errorf("err = %v, want ErrNotFound (disabled)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMBCAAProvider_Configure_TogglesEnabled(t *testing.T) {
|
||||
resetRegistryForTests()
|
||||
defer resetRegistryForTests()
|
||||
p := &mbcaaProvider{fetcher: NewFetcher(FetcherConfig{})}
|
||||
|
||||
if err := p.Configure(ProviderSettings{Enabled: true}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
if !p.enabled.Load() {
|
||||
t.Error("after Configure(Enabled: true), enabled = false")
|
||||
}
|
||||
|
||||
if err := p.Configure(ProviderSettings{Enabled: false}); err != nil {
|
||||
t.Fatalf("Configure: %v", err)
|
||||
}
|
||||
if p.enabled.Load() {
|
||||
t.Error("after Configure(Enabled: false), enabled = true")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMBCAAProvider_TestConnection_TreatsNotFoundAsSuccess(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "not found", http.StatusNotFound)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := newMBCAAProviderForTest(t, srv.URL)
|
||||
if err := p.TestConnection(context.Background()); err != nil {
|
||||
t.Errorf("TestConnection err = %v, want nil (404 should be treated as connection ok)", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMBCAAProvider_TestConnection_PropagatesTransient(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
http.Error(w, "boom", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
p := newMBCAAProviderForTest(t, srv.URL)
|
||||
if err := p.TestConnection(context.Background()); err == nil {
|
||||
t.Error("TestConnection err = nil, want non-nil for 500")
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewMBCAAProviderFromConfig_ReplacesRegisteredFetcher(t *testing.T) {
|
||||
resetRegistryForTests()
|
||||
defer resetRegistryForTests()
|
||||
|
||||
// Pre-register with default config (mimics init() behaviour).
|
||||
Register(&mbcaaProvider{fetcher: NewFetcher(FetcherConfig{})})
|
||||
|
||||
cfg := FetcherConfig{
|
||||
UserAgent: "Minstrel/test-replaced (x)",
|
||||
MinPeriod: 100 * time.Millisecond,
|
||||
}
|
||||
NewMBCAAProviderFromConfig(cfg)
|
||||
|
||||
p, err := ProviderByID("mbcaa")
|
||||
if err != nil {
|
||||
t.Fatalf("ProviderByID: %v", err)
|
||||
}
|
||||
mp := p.(*mbcaaProvider)
|
||||
if mp.fetcher == nil {
|
||||
t.Fatal("fetcher is nil after NewMBCAAProviderFromConfig")
|
||||
}
|
||||
// We can't peek the fetcher's UserAgent directly (it's an unexported
|
||||
// field on FetcherConfig held inside Fetcher), but the side effect of
|
||||
// ProviderByID returning the same registered instance with a swapped
|
||||
// fetcher is what we're verifying here.
|
||||
}
|
||||
Reference in New Issue
Block a user