Files
minstrel/internal/api/admin_cover_sources_test.go
T
bvandeusenandClaude Opus 5.5 08a2c06b85 docs: api comments say what the endpoints do, not how they came to be (#6277)
Rewrites the internal/api comment blocks to rule 32: task, milestone and
rule ids gone, the stories behind them restated as the constraint they
left, stale references corrected (the stream secret is wired from config,
the timezone handler already refreshes the scheduler, the Android app is
the native client, the design docs cited are not in the repo). Also
rewraps comment lines over 80 columns left by the earlier batches.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-23 12:23:58 -04:00

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