Files
minstrel/internal/coverart/settings_test.go
T
bvandeusen 392b8aa12a feat(server/m7-cover-sources): SettingsService
Reconciles the compile-time provider registry with the
cover_art_provider_settings table at boot, and on every admin
update. Calls Configure() on each provider with the row data so
runtime config (enabled, api_key) propagates immediately.

Owns the cover_art_sources_meta.current_version stamp.
UpdateProvider returns versionBumped=true only when the change
altered the *enabled set* of providers — key rotations don't bump
version (the recheck trigger is for "we have a new source to ask,"
not config rotations).

Capability filtering: EnabledAlbumProviders / EnabledArtistProviders
return only registered providers that (a) are enabled per DB and
(b) implement the requested capability interface. Snapshots —
callers don't mutate.

ListProviderInfo builds the wire-shaped slice the admin GET handler
returns. APIKeySet bool replaces echoing the key (credential never
leaves the server).

TestProvider dispatches to TestableProvider.TestConnection on the
named provider, ErrNotTestable if the provider doesn't implement
the capability, ErrProviderNotFound if not registered.

UpdateProviderSettingsParams uses the sqlc-generated field names:
Enabled bool (not *bool — the COALESCE is handled by passing the
cached current value when patch.Enabled is nil), Column3 bool
(apiKeyChanged flag), ApiKey *string (the api_key value — sqlc
named this from the column, not Column4 as the task spec assumed).

Tests gated on MINSTREL_TEST_DATABASE_URL: boot-inserts-defaults,
flip-enabled-bumps-version, key-only-doesn't-bump, clear-key,
TestProvider routing for testable / non-testable / missing,
ListProviderInfo capability badges. Reuses newPool / discardLogger
and fakeProvider / fakeAlbumProvider from enricher_test.go and
provider_test.go (same package).
2026-05-06 12:43:54 -04:00

234 lines
6.8 KiB
Go

package coverart
import (
"context"
"errors"
"testing"
)
// fakeArtistProvider opts into ArtistArtProvider for capability tests.
type fakeArtistProvider struct {
fakeProvider
}
func (f *fakeArtistProvider) FetchArtistArt(_ context.Context, _ string) (thumb, fanart []byte, err error) {
return []byte("t"), []byte("f"), nil
}
// fakeTestableAlbumProvider implements TestableProvider for the
// TestProvider test.
type fakeTestableAlbumProvider struct {
fakeAlbumProvider
testErr error
}
func (f *fakeTestableAlbumProvider) TestConnection(_ context.Context) error {
return f.testErr
}
func TestSettingsService_BootInsertsDefaultRows(t *testing.T) {
pool := newPool(t)
resetRegistryForTests()
defer resetRegistryForTests()
Register(&fakeAlbumProvider{fakeProvider: fakeProvider{id: "fake-album", display: "Fake Album", defaultOn: true}})
Register(&fakeArtistProvider{fakeProvider: fakeProvider{id: "fake-artist", display: "Fake Artist", defaultOn: false}})
s, err := NewSettingsService(context.Background(), pool, discardLogger())
if err != nil {
t.Fatalf("NewSettingsService: %v", err)
}
// Both providers should now have rows; only fake-album is enabled.
enabled := s.EnabledAlbumProviders()
if len(enabled) != 1 {
t.Fatalf("EnabledAlbumProviders = %d, want 1", len(enabled))
}
if enabled[0].ID() != "fake-album" {
t.Errorf("enabled[0].ID = %q, want fake-album", enabled[0].ID())
}
// fake-artist defaultOn=false, so it should NOT be in EnabledArtistProviders.
artists := s.EnabledArtistProviders()
if len(artists) != 0 {
t.Errorf("EnabledArtistProviders = %d, want 0", len(artists))
}
if s.CurrentVersion() != 1 {
t.Errorf("CurrentVersion = %d, want 1", s.CurrentVersion())
}
}
func TestSettingsService_UpdateProvider_FlippingEnabledBumpsVersion(t *testing.T) {
pool := newPool(t)
resetRegistryForTests()
defer resetRegistryForTests()
Register(&fakeAlbumProvider{fakeProvider: fakeProvider{id: "fake-album", defaultOn: true}})
s, err := NewSettingsService(context.Background(), pool, discardLogger())
if err != nil {
t.Fatal(err)
}
startVersion := s.CurrentVersion()
disabled := false
bumped, err := s.UpdateProvider(context.Background(), "fake-album", ProviderUpdatePatch{Enabled: &disabled})
if err != nil {
t.Fatalf("UpdateProvider: %v", err)
}
if !bumped {
t.Error("versionBumped = false, want true after disabling")
}
if s.CurrentVersion() <= startVersion {
t.Errorf("CurrentVersion = %d, want > %d", s.CurrentVersion(), startVersion)
}
enabled := s.EnabledAlbumProviders()
if len(enabled) != 0 {
t.Errorf("after disable: EnabledAlbumProviders = %d, want 0", len(enabled))
}
}
func TestSettingsService_UpdateProvider_KeyOnlyDoesNotBumpVersion(t *testing.T) {
pool := newPool(t)
resetRegistryForTests()
defer resetRegistryForTests()
Register(&fakeAlbumProvider{fakeProvider: fakeProvider{id: "fake-album", defaultOn: true}})
s, err := NewSettingsService(context.Background(), pool, discardLogger())
if err != nil {
t.Fatal(err)
}
startVersion := s.CurrentVersion()
newKey := "abc123"
bumped, err := s.UpdateProvider(context.Background(), "fake-album", ProviderUpdatePatch{APIKey: &newKey})
if err != nil {
t.Fatalf("UpdateProvider: %v", err)
}
if bumped {
t.Error("versionBumped = true, want false for key-only change")
}
if s.CurrentVersion() != startVersion {
t.Errorf("CurrentVersion changed: %d → %d, want unchanged", startVersion, s.CurrentVersion())
}
infos := s.ListProviderInfo()
var info ProviderInfo
for _, i := range infos {
if i.ID == "fake-album" {
info = i
break
}
}
if !info.APIKeySet {
t.Error("APIKeySet = false, want true after setting key")
}
}
func TestSettingsService_UpdateProvider_ClearKey(t *testing.T) {
pool := newPool(t)
resetRegistryForTests()
defer resetRegistryForTests()
Register(&fakeAlbumProvider{fakeProvider: fakeProvider{id: "fake-album", defaultOn: true}})
s, err := NewSettingsService(context.Background(), pool, discardLogger())
if err != nil {
t.Fatal(err)
}
// Set a key, then clear it.
setKey := "abc"
if _, err := s.UpdateProvider(context.Background(), "fake-album", ProviderUpdatePatch{APIKey: &setKey}); err != nil {
t.Fatal(err)
}
emptyKey := ""
if _, err := s.UpdateProvider(context.Background(), "fake-album", ProviderUpdatePatch{APIKey: &emptyKey}); err != nil {
t.Fatal(err)
}
infos := s.ListProviderInfo()
for _, i := range infos {
if i.ID == "fake-album" && i.APIKeySet {
t.Error("APIKeySet = true after clear, want false")
}
}
}
func TestSettingsService_TestProvider_Routing(t *testing.T) {
pool := newPool(t)
resetRegistryForTests()
defer resetRegistryForTests()
plain := &fakeAlbumProvider{fakeProvider: fakeProvider{id: "plain", defaultOn: true}}
testable := &fakeTestableAlbumProvider{
fakeAlbumProvider: fakeAlbumProvider{fakeProvider: fakeProvider{id: "testable", defaultOn: true}},
testErr: nil,
}
Register(plain)
Register(testable)
s, err := NewSettingsService(context.Background(), pool, discardLogger())
if err != nil {
t.Fatal(err)
}
// Testable returns nil → success.
if err := s.TestProvider(context.Background(), "testable"); err != nil {
t.Errorf("TestProvider(testable) = %v, want nil", err)
}
// Plain doesn't implement TestableProvider → ErrNotTestable.
if err := s.TestProvider(context.Background(), "plain"); !errors.Is(err, ErrNotTestable) {
t.Errorf("TestProvider(plain) = %v, want ErrNotTestable", err)
}
// Missing → ErrProviderNotFound.
if err := s.TestProvider(context.Background(), "missing"); !errors.Is(err, ErrProviderNotFound) {
t.Errorf("TestProvider(missing) = %v, want ErrProviderNotFound", err)
}
}
func TestSettingsService_ListProviderInfo_Capabilities(t *testing.T) {
pool := newPool(t)
resetRegistryForTests()
defer resetRegistryForTests()
plain := &fakeProvider{id: "plain", defaultOn: true}
album := &fakeAlbumProvider{fakeProvider: fakeProvider{id: "album-only", defaultOn: true}}
artist := &fakeArtistProvider{fakeProvider: fakeProvider{id: "artist-only", defaultOn: true}}
Register(plain)
Register(album)
Register(artist)
s, err := NewSettingsService(context.Background(), pool, discardLogger())
if err != nil {
t.Fatal(err)
}
infos := s.ListProviderInfo()
if len(infos) != 3 {
t.Fatalf("ListProviderInfo len = %d, want 3", len(infos))
}
byID := map[string]ProviderInfo{}
for _, i := range infos {
byID[i.ID] = i
}
if got := byID["plain"].Supports; len(got) != 0 {
t.Errorf("plain.Supports = %v, want empty", got)
}
if got := byID["album-only"].Supports; len(got) != 1 || got[0] != "album_cover" {
t.Errorf("album-only.Supports = %v, want [album_cover]", got)
}
if got := byID["artist-only"].Supports; len(got) != 2 || got[0] != "artist_thumb" || got[1] != "artist_fanart" {
t.Errorf("artist-only.Supports = %v, want [artist_thumb, artist_fanart]", got)
}
}