385eb3b163
Rewires EnrichAlbum to iterate EnabledAlbumProviders from the SettingsService instead of a single hardcoded MBCAA fetcher. Sidecar layer is unchanged (always tried first). Per-row eligibility check honors cover_art_sources_version: terminal 'found' values skip; 'none' flips to NULL via ClearAlbumCoverNone and proceeds (the DB- level ListAlbumsMissingCover query guards the version check for batch callers; RetryAlbum clears via ClearAlbumCover first); NULL is eligible. The provider chain uses an inline allWere404 accumulator to settle the row to 'none' only when every provider returned ErrNotFound. Any provider returning ErrTransient leaves the row NULL for next- pass retry — even if other providers returned ErrNotFound — since the transient call's MBID might succeed on a future attempt. Boot wiring (cmd/minstrel/main.go) replaces the NewFetcher+NewEnricher(fetcher, mbcaaOn) shape with NewMBCAAProviderFromConfig (swaps in production User-Agent on the registered provider) + NewSettingsService + NewEnricher(settings). The old cfg.Library.CoverArtFromMBCAA flag is no-op'd; DB-backed settings replace it. Field stays in the config struct for backward compat with deployed config files. Consolidates fetcher.go's HTTP logic into provider_mbcaa.go (no more wrapper indirection): mbcaaProvider now holds cfg/mu/lastCall/client directly. Deletes fetcher.go and fetcher_test.go. ErrNotFound and ErrTransient move into provider.go alongside the other sentinels. Updates admin_covers_test.go and provider_mbcaa_test.go to the new constructor shapes. Adds multi-provider-chain, transient-leaves-null, and stale-none-flips-and-retries test cases in enricher_test.go. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
134 lines
4.6 KiB
Go
134 lines
4.6 KiB
Go
package api
|
|
|
|
import (
|
|
"encoding/json"
|
|
"log/slog"
|
|
"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"
|
|
"git.fabledsword.com/bvandeusen/minstrel/internal/db/dbq"
|
|
)
|
|
|
|
// newAdminCoversRouter builds a test chi router with the two cover admin
|
|
// endpoints. RequireAdmin middleware is applied; tests inject the user into
|
|
// context manually (RequireUser is bypassed).
|
|
func newAdminCoversRouter(h *handlers) chi.Router {
|
|
r := chi.NewRouter()
|
|
r.Route("/api/admin", func(admin chi.Router) {
|
|
admin.Use(auth.RequireAdmin())
|
|
admin.Post("/albums/{id}/cover/refetch", h.handleAdminAlbumRefetchCover)
|
|
admin.Post("/covers/refetch-missing", h.handleAdminBulkRefetchCovers)
|
|
})
|
|
return r
|
|
}
|
|
|
|
// doAdminCoversReq fires an HTTP request against the admin covers router
|
|
// with the given user in context.
|
|
func doAdminCoversReq(t *testing.T, h *handlers, method, path string, user dbq.User) *httptest.ResponseRecorder {
|
|
t.Helper()
|
|
req := httptest.NewRequest(method, path, nil)
|
|
req = withUser(req, user)
|
|
w := httptest.NewRecorder()
|
|
newAdminCoversRouter(h).ServeHTTP(w, req)
|
|
return w
|
|
}
|
|
|
|
// testHandlersWithCovers returns a handlers instance with a stub coverart
|
|
// Enricher wired in. No external providers are registered, so the
|
|
// sidecar-only path is exercised and cover_art_source lands as 'none' for
|
|
// albums without real track files.
|
|
func testHandlersWithCovers(t *testing.T) (*handlers, *coverart.Enricher) {
|
|
t.Helper()
|
|
h, pool := testHandlers(t)
|
|
settings, err := coverart.NewSettingsService(t.Context(), pool, slog.Default())
|
|
if err != nil {
|
|
t.Fatalf("NewSettingsService: %v", err)
|
|
}
|
|
enricher := coverart.NewEnricher(pool, slog.Default(), settings)
|
|
h.coverart = enricher
|
|
h.coverArtBackfillCap = 100
|
|
return h, enricher
|
|
}
|
|
|
|
func TestAdminAlbumRefetchCover_NonAdminReturns403(t *testing.T) {
|
|
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
|
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
|
}
|
|
h, _ := testHandlersWithCovers(t)
|
|
user := seedUser(t, h.pool, "alice", "pw", false)
|
|
artist := seedArtist(t, h.pool, "TestArtist")
|
|
album := seedAlbum(t, h.pool, artist.ID, "TestAlbum", 0)
|
|
|
|
w := doAdminCoversReq(t, h, http.MethodPost,
|
|
"/api/admin/albums/"+uuidToString(album.ID)+"/cover/refetch", user)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want 403; body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|
|
|
|
func TestAdminAlbumRefetchCover_AdminOK(t *testing.T) {
|
|
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
|
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
|
}
|
|
h, _ := testHandlersWithCovers(t)
|
|
admin := seedUser(t, h.pool, "rootuser", "pw", true)
|
|
artist := seedArtist(t, h.pool, "TestArtist")
|
|
album := seedAlbum(t, h.pool, artist.ID, "TestAlbum", 0)
|
|
|
|
w := doAdminCoversReq(t, h, http.MethodPost,
|
|
"/api/admin/albums/"+uuidToString(album.ID)+"/cover/refetch", admin)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
}
|
|
var resp adminCoverRefetchResp
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
// Album has no MBID and no sidecar, so source should be 'none'.
|
|
if resp.CoverArtSource == nil || *resp.CoverArtSource != "none" {
|
|
t.Errorf("source = %v, want 'none'", resp.CoverArtSource)
|
|
}
|
|
}
|
|
|
|
func TestAdminBulkRefetchCovers_AdminReturnsQueuedCount(t *testing.T) {
|
|
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
|
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
|
}
|
|
h, _ := testHandlersWithCovers(t)
|
|
admin := seedUser(t, h.pool, "rootuser2", "pw", true)
|
|
artist := seedArtist(t, h.pool, "TestArtist")
|
|
_ = seedAlbum(t, h.pool, artist.ID, "Album1", 0)
|
|
_ = seedAlbum(t, h.pool, artist.ID, "Album2", 0)
|
|
|
|
w := doAdminCoversReq(t, h, http.MethodPost, "/api/admin/covers/refetch-missing", admin)
|
|
if w.Code != http.StatusOK {
|
|
t.Errorf("status = %d, want 200; body=%s", w.Code, w.Body.String())
|
|
}
|
|
var resp adminBulkRefetchResp
|
|
if err := json.Unmarshal(w.Body.Bytes(), &resp); err != nil {
|
|
t.Fatalf("decode: %v", err)
|
|
}
|
|
if resp.Queued != h.coverArtBackfillCap {
|
|
t.Errorf("queued = %d, want %d (cap)", resp.Queued, h.coverArtBackfillCap)
|
|
}
|
|
}
|
|
|
|
func TestAdminBulkRefetchCovers_NonAdminReturns403(t *testing.T) {
|
|
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
|
|
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
|
|
}
|
|
h, _ := testHandlersWithCovers(t)
|
|
user := seedUser(t, h.pool, "alicebulk", "pw", false)
|
|
|
|
w := doAdminCoversReq(t, h, http.MethodPost, "/api/admin/covers/refetch-missing", user)
|
|
if w.Code != http.StatusForbidden {
|
|
t.Errorf("status = %d, want 403; body=%s", w.Code, w.Body.String())
|
|
}
|
|
}
|