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") } }