392b8aa12a
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).
234 lines
6.8 KiB
Go
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)
|
|
}
|
|
}
|