Files
minstrel/internal/api/admin_coverage_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

158 lines
5.5 KiB
Go

package api
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"testing"
"github.com/go-chi/chi/v5"
"git.fabledsword.com/bvandeusen/minstrel/internal/auth"
)
// newAdminCoverageRouter builds a test chi router exposing only the
// coverage endpoint, mirroring the pattern in admin_scan_test.go.
func newAdminCoverageRouter(h *handlers) chi.Router {
r := chi.NewRouter()
r.Route("/api/admin", func(admin chi.Router) {
admin.Use(auth.RequireAdmin())
admin.Get("/library/coverage", h.handleGetLibraryCoverage)
})
return r
}
func TestAdminLibraryCoverage_EmptyLibraryReturnsZeros(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
h, pool := testHandlers(t)
admin := seedUser(t, pool, "covgauge1", "pw", true)
req := httptest.NewRequest(http.MethodGet, "/api/admin/library/coverage", nil)
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverageRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp coverageRollupResp
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.Total != 0 || resp.WithArt != 0 || resp.Pending != 0 || resp.Settled != 0 || resp.PendingNoMbid != 0 {
t.Errorf("expected all zeros on empty library; got %+v", resp)
}
}
func TestAdminLibraryCoverage_MixedRowsReturnCorrectBuckets(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
h, pool := testHandlers(t)
admin := seedUser(t, pool, "covgauge2", "pw", true)
// Seed an artist for FK satisfaction.
artist := seedArtist(t, pool, "Coverage Test")
// Seed albums covering each bucket. Use distinct titles to avoid the
// unique (artist_id, title) constraint. Distinct mbids where set so
// the partial unique index on mbid doesn't trip.
type seed struct {
title string
source *string
mbid *string
}
sourceNone := "none"
sourceSidecar := "sidecar"
sourceMbcaa := "mbcaa"
sourceEmbedded := "embedded"
sourceTheaudiodb := "theaudiodb"
sourceDeezer := "deezer"
sourceLastfm := "lastfm"
mbid1 := "11111111-1111-1111-1111-111111111111"
mbid2 := "22222222-2222-2222-2222-222222222222"
mbid3 := "33333333-3333-3333-3333-333333333333"
mbid4 := "44444444-4444-4444-4444-444444444444"
mbid5 := "55555555-5555-5555-5555-555555555555"
mbid6 := "66666666-6666-6666-6666-666666666666"
mbid7 := "77777777-7777-7777-7777-777777777777"
// Cover every cover_art_source value the migrations allow, so a source
// added to the CHECK but not to the rollup query's whitelist trips this
// test.
rows := []seed{
{title: "WithArtSidecar", source: &sourceSidecar, mbid: &mbid1}, // with_art
{title: "WithArtMbcaa", source: &sourceMbcaa, mbid: &mbid2}, // with_art
{title: "WithArtEmbedded", source: &sourceEmbedded, mbid: &mbid4}, // with_art
{title: "WithArtTheaudiodb", source: &sourceTheaudiodb, mbid: &mbid5}, // with_art
{title: "WithArtDeezer", source: &sourceDeezer, mbid: &mbid6}, // with_art
{title: "WithArtLastfm", source: &sourceLastfm, mbid: &mbid7}, // with_art
{title: "PendingHasMbid", source: nil, mbid: &mbid3}, // pending (eligible)
{title: "PendingNoMbid", source: nil, mbid: nil}, // pending + pending_no_mbid
{title: "Settled", source: &sourceNone, mbid: nil}, // settled (no mbid is fine here)
}
for _, s := range rows {
if _, err := pool.Exec(context.Background(), `
INSERT INTO albums (artist_id, title, sort_title, cover_art_source, mbid)
VALUES ($1, $2, $3, $4, $5)
`, artist.ID, s.title, s.title, s.source, s.mbid); err != nil {
t.Fatalf("seed album %q: %v", s.title, err)
}
}
req := httptest.NewRequest(http.MethodGet, "/api/admin/library/coverage", nil)
req = withUser(req, admin)
rec := httptest.NewRecorder()
newAdminCoverageRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("status = %d, want 200; body=%s", rec.Code, rec.Body.String())
}
var resp coverageRollupResp
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("decode: %v", err)
}
if resp.Total != 9 {
t.Errorf("Total = %d, want 9", resp.Total)
}
// Six with_art rows — one per valid cover_art_source value
// (sidecar, mbcaa, embedded, theaudiodb, deezer, lastfm).
if resp.WithArt != 6 {
t.Errorf("WithArt = %d, want 6", resp.WithArt)
}
if resp.Pending != 2 {
t.Errorf("Pending = %d, want 2", resp.Pending)
}
if resp.Settled != 1 {
t.Errorf("Settled = %d, want 1", resp.Settled)
}
if resp.PendingNoMbid != 1 {
t.Errorf("PendingNoMbid = %d, want 1", resp.PendingNoMbid)
}
// Invariant: with_art + pending + settled == total.
if resp.WithArt+resp.Pending+resp.Settled != resp.Total {
t.Errorf("invariant violated: %d + %d + %d != %d",
resp.WithArt, resp.Pending, resp.Settled, resp.Total)
}
}
func TestAdminLibraryCoverage_NonAdminReturns403(t *testing.T) {
if os.Getenv("MINSTREL_TEST_DATABASE_URL") == "" {
t.Skip("MINSTREL_TEST_DATABASE_URL not set")
}
h, pool := testHandlers(t)
user := seedUser(t, pool, "covgauge3", "pw", false)
req := httptest.NewRequest(http.MethodGet, "/api/admin/library/coverage", nil)
req = withUser(req, user)
rec := httptest.NewRecorder()
newAdminCoverageRouter(h).ServeHTTP(rec, req)
if rec.Code != http.StatusForbidden {
t.Fatalf("status = %d, want 403; body=%s", rec.Code, rec.Body.String())
}
}