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>
266 lines
8.9 KiB
Go
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")
|
|
}
|
|
}
|