Files
minstrel/internal/api/admin_cover_sources_test.go
bvandeusen 5e91efe695 test(coverart): fix registry-wipe + stale provider signature
Two distinct pre-existing test bugs in the last failing cluster:

1. newTestEnricher() called resetRegistryForTests() itself, wiping the
   fake providers callers Register() right before calling it (its own
   doc says callers register first and reconcile() picks them up). The
   ~8 TestEnrichArtist_* failures (source stayed NULL, no thumb
   written) all stem from reconcile() seeing an empty registry. Remove
   the internal reset; make every caller that lacked one own the
   registry lifecycle explicitly (resetRegistryForTests + t.Cleanup):
   SidecarFound, NoSidecarNoMBID, AlreadySidecar_NoOp,
   DrainsNullSourceOnly.

2. apiTestAlbumProvider.FetchAlbumCover had a stale signature
   (context, string) predating the AlbumRef refactor; it no longer
   satisfied coverart.AlbumCoverProvider, so the p.(AlbumCoverProvider)
   capability assertion failed and TestAdminListCoverSources got
   supports=[]. Fix the param to coverart.AlbumRef.

Test-only. (A1/A2 were correct; they unmasked these. Any residual
album-enricher semantics failures will be root-caused from the next
clean run, not bundled speculatively.)

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 21:05:36 -04:00

265 lines
8.9 KiB
Go

package api
import (
"bytes"
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/go-chi/chi/v5"
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
"git.fabledsword.com/bvandeusen/minstrel/internal/coverart"
)
// apiTestAlbumProvider is a minimal AlbumCoverProvider for use in api
// package tests. Does not implement TestableProvider.
type apiTestAlbumProvider struct {
id string
display string
}
func (p *apiTestAlbumProvider) ID() string { return p.id }
func (p *apiTestAlbumProvider) DisplayName() string { return p.display }
func (p *apiTestAlbumProvider) RequiresAPIKey() bool { return false }
func (p *apiTestAlbumProvider) DefaultEnabled() bool { return true }
func (p *apiTestAlbumProvider) Configure(_ coverart.ProviderSettings) error { return nil }
func (p *apiTestAlbumProvider) FetchAlbumCover(_ context.Context, _ coverart.AlbumRef) ([]byte, error) {
return []byte("img"), nil
}
// apiTestTestableProvider is an AlbumCoverProvider that also implements
// TestableProvider (so the test connection button exercises the success path).
type apiTestTestableProvider struct {
apiTestAlbumProvider
}
func (p *apiTestTestableProvider) TestConnection(_ context.Context) error { return nil }
func newAdminCoverSourcesRouter(h *handlers) chi.Router {
r := chi.NewRouter()
r.Route("/api/admin", func(admin chi.Router) {
admin.Use(auth.RequireAdmin())
admin.Get("/cover-sources", h.handleListCoverSources)
admin.Patch("/cover-sources/{provider_id}", h.handleUpdateCoverSource)
admin.Post("/cover-sources/{provider_id}/test", h.handleTestCoverSource)
})
return r
}
// testHandlersWithCoverSettings extends testHandlers by installing a
// SettingsService backed by the test DB. Caller is responsible for
// registering whichever fake providers the test needs BEFORE calling
// this helper, via coverart.Register. Use coverart.ResetRegistryForTests
// with t.Cleanup to restore registry state after the test.
func testHandlersWithCoverSettings(t *testing.T) (*handlers, *coverart.SettingsService) {
t.Helper()
h, pool := testHandlers(t)
s, err := coverart.NewSettingsService(context.Background(), pool, h.logger)
if err != nil {
t.Fatalf("NewSettingsService: %v", err)
}
h.coverSettings = s
return h, s
}
func TestAdminListCoverSources_ReturnsRegisteredProviders(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
coverart.ResetRegistryForTests()
t.Cleanup(coverart.ResetRegistryForTests)
coverart.Register(&apiTestAlbumProvider{id: "test-album-prov", display: "Test Album Provider"})
h, _ := testHandlersWithCoverSettings(t)
admin := seedUser(t, h.pool, "cslist", "pw", true)
req := httptest.NewRequest(http.MethodGet, "/api/admin/cover-sources", nil)
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverSourcesRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp coverProvidersListResp
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if len(resp.Providers) != 1 {
t.Fatalf("providers len = %d, want 1", len(resp.Providers))
}
p := resp.Providers[0]
if p.ID != "test-album-prov" {
t.Errorf("ID = %q, want test-album-prov", p.ID)
}
if p.DisplayName != "Test Album Provider" {
t.Errorf("DisplayName = %q, want Test Album Provider", p.DisplayName)
}
if p.APIKeySet {
t.Error("api_key_set = true, want false (no key set)")
}
// apiTestAlbumProvider implements AlbumCoverProvider but not TestableProvider.
if p.Testable {
t.Error("testable = true, want false")
}
found := false
for _, s := range p.Supports {
if s == "album_cover" {
found = true
}
}
if !found {
t.Errorf("supports = %v, want to include album_cover", p.Supports)
}
}
func TestAdminUpdateCoverSource_FlippingEnabledBumpsVersion(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
coverart.ResetRegistryForTests()
t.Cleanup(coverart.ResetRegistryForTests)
coverart.Register(&apiTestAlbumProvider{id: "flip-prov", display: "Flip Provider"})
h, _ := testHandlersWithCoverSettings(t)
admin := seedUser(t, h.pool, "csflip", "pw", true)
disabled := false
body, _ := json.Marshal(updateCoverSourceReq{Enabled: &disabled})
req := httptest.NewRequest(http.MethodPatch, "/api/admin/cover-sources/flip-prov", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverSourcesRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp updateCoverSourceResp
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if !resp.VersionBumped {
t.Error("version_bumped = false, want true after flipping enabled")
}
}
func TestAdminUpdateCoverSource_KeyOnlyDoesNotBump(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
coverart.ResetRegistryForTests()
t.Cleanup(coverart.ResetRegistryForTests)
coverart.Register(&apiTestAlbumProvider{id: "key-prov", display: "Key Provider"})
h, _ := testHandlersWithCoverSettings(t)
admin := seedUser(t, h.pool, "cskey", "pw", true)
newKey := "newkey"
body, _ := json.Marshal(updateCoverSourceReq{APIKey: &newKey})
req := httptest.NewRequest(http.MethodPatch, "/api/admin/cover-sources/key-prov", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverSourcesRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp updateCoverSourceResp
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.VersionBumped {
t.Error("version_bumped = true, want false for key-only change")
}
if !resp.APIKeySet {
t.Error("api_key_set = false, want true after setting key")
}
}
func TestAdminUpdateCoverSource_UnknownProvider404(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
coverart.ResetRegistryForTests()
t.Cleanup(coverart.ResetRegistryForTests)
// No providers registered — anything we patch will be not-found.
h, _ := testHandlersWithCoverSettings(t)
admin := seedUser(t, h.pool, "cs404", "pw", true)
body, _ := json.Marshal(updateCoverSourceReq{})
req := httptest.NewRequest(http.MethodPatch, "/api/admin/cover-sources/missing", bytes.NewReader(body))
req.Header.Set("Content-Type", "application/json")
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverSourcesRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404; body=%s", rec.Code, rec.Body.String())
}
}
func TestAdminTestCoverSource_NonAdminReturns403(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
coverart.ResetRegistryForTests()
t.Cleanup(coverart.ResetRegistryForTests)
coverart.Register(&apiTestTestableProvider{apiTestAlbumProvider{id: "testable-prov", display: "Testable"}})
h, _ := testHandlersWithCoverSettings(t)
nonAdmin := seedUser(t, h.pool, "csnoadmin", "pw", false)
req := httptest.NewRequest(http.MethodPost, "/api/admin/cover-sources/testable-prov/test", nil)
req = withUser(req, nonAdmin)
rec := httptest.NewRecorder()
newAdminCoverSourcesRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403; body=%s", rec.Code, rec.Body.String())
}
}
func TestAdminTestCoverSource_NotTestableReturnsOkFalse(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
coverart.ResetRegistryForTests()
t.Cleanup(coverart.ResetRegistryForTests)
// apiTestAlbumProvider does NOT implement TestableProvider.
coverart.Register(&apiTestAlbumProvider{id: "nontestable-prov", display: "Non-Testable"})
h, _ := testHandlersWithCoverSettings(t)
admin := seedUser(t, h.pool, "csntest", "pw", true)
req := httptest.NewRequest(http.MethodPost, "/api/admin/cover-sources/nontestable-prov/test", nil)
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverSourcesRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp testCoverSourceResp
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.OK {
t.Error("ok = true, want false for non-testable provider")
}
if resp.Error == "" {
t.Error("error string empty, want message mentioning test connection")
}
}