package coverart import ( "context" "errors" "testing" ) // fakeArtistProvider opts into ArtistArtProvider for capability tests. type fakeArtistProvider struct { fakeProvider } func (f *fakeArtistProvider) FetchArtistArt(_ context.Context, _ ArtistRef) (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) } } // TestSettingsService_BumpVersionIfProvidersChanged_FirstBoot: stored // hash is empty (column default ""), so a real provider set produces a bump. func TestSettingsService_BumpVersionIfProvidersChanged_FirstBoot(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.Fatalf("init: %v", err) } before := s.CurrentVersion() newVer, bumped, err := s.BumpVersionIfProvidersChanged(context.Background()) if err != nil { t.Fatalf("bump: %v", err) } if !bumped { t.Errorf("bumped = false, want true on first boot (stored hash is empty)") } if newVer <= before { t.Errorf("newVer = %d, want > %d", newVer, before) } if s.CurrentVersion() != newVer { t.Errorf("in-memory version = %d, want %d (synchronized with DB)", s.CurrentVersion(), newVer) } } // TestSettingsService_BumpVersionIfProvidersChanged_NoChange: second // call back-to-back returns bumped=false because the stored hash now // matches the registered set. func TestSettingsService_BumpVersionIfProvidersChanged_NoChange(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.Fatalf("init: %v", err) } // First call bumps (empty stored hash → mismatch). if _, _, err := s.BumpVersionIfProvidersChanged(context.Background()); err != nil { t.Fatalf("first bump: %v", err) } afterFirst := s.CurrentVersion() _, bumped, err := s.BumpVersionIfProvidersChanged(context.Background()) if err != nil { t.Fatalf("second bump: %v", err) } if bumped { t.Errorf("bumped = true on second call, want false (no change)") } if s.CurrentVersion() != afterFirst { t.Errorf("version changed without a provider-set change") } } // TestSettingsService_BumpVersion_Increments verifies the manual bump // path. Distinct from BumpVersionIfProvidersChanged, this always writes // regardless of the stored provider-set hash. func TestSettingsService_BumpVersion_Increments(t *testing.T) { pool := newPool(t) resetRegistryForTests() defer resetRegistryForTests() Register(&fakeAlbumProvider{fakeProvider: fakeProvider{id: "fake-album-bump", defaultOn: true}}) s, err := NewSettingsService(context.Background(), pool, discardLogger()) if err != nil { t.Fatalf("init: %v", err) } before := s.CurrentVersion() newVer, err := s.BumpVersion(context.Background()) if err != nil { t.Fatalf("bump: %v", err) } if newVer <= before { t.Errorf("newVer = %d, want > %d", newVer, before) } if s.CurrentVersion() != newVer { t.Errorf("in-memory version = %d, want %d", s.CurrentVersion(), newVer) } // Calling again increments by another 1 (always writes). afterFirst := newVer newVer2, err := s.BumpVersion(context.Background()) if err != nil { t.Fatalf("second bump: %v", err) } if newVer2 != afterFirst+1 { t.Errorf("second newVer = %d, want %d", newVer2, afterFirst+1) } } // TestRegisteredProvidersHash_StableAcrossOrder: registry order // shouldn't matter because we sort before hashing. func TestRegisteredProvidersHash_StableAcrossOrder(t *testing.T) { // Hash is a function of the registered set; in this test it just // needs to be deterministic and non-empty. h1 := registeredProvidersHash() h2 := registeredProvidersHash() if h1 != h2 { t.Errorf("hash is not deterministic: %q vs %q", h1, h2) } if h1 == "" { t.Errorf("hash empty; expected at least one provider registered (or empty set is valid)") } if len(h1) != 64 { t.Errorf("hash length = %d, want 64 (SHA-256 hex)", len(h1)) } }