fix(server,web/m7-cover-sources): forward-fix CI round 2

Three issues from the previous push:

1. internal/api/admin_covers_test.go used t.Context() (Go 1.24+) but
   go.mod declares go 1.23.0. Replace with context.Background() in
   the testHandlersWithCovers helper; add "context" import.

2. internal/server/server_test.go's New() call missed the
   *coverart.SettingsService argument added in T11. The new param
   sits between the cover-art-backfill cap (int) and the *library.Scanner
   pointer in the function signature; the test was using 12 args
   instead of 13. Insert nil at the right position in all six call sites.

3. integrations.test.ts had 11 failing tests after T13 added the
   cover-art-providers section. Both panels render "Save changes",
   "Test connection", and "API key" controls, so the existing
   Lidarr tests' role/text queries matched multiple elements. Add
   helper functions (lidarrSection, coverProvidersSection,
   providerCard) and scope every relevant query with within() so
   each panel's tests interact only with their own controls.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-06 13:55:35 -04:00
parent 5c386dc73e
commit 62222d8438
3 changed files with 45 additions and 27 deletions
+2 -1
View File
@@ -1,6 +1,7 @@
package api package api
import ( import (
"context"
"encoding/json" "encoding/json"
"log/slog" "log/slog"
"net/http" "net/http"
@@ -46,7 +47,7 @@ func doAdminCoversReq(t *testing.T, h *handlers, method, path string, user dbq.U
func testHandlersWithCovers(t *testing.T) (*handlers, *coverart.Enricher) { func testHandlersWithCovers(t *testing.T) (*handlers, *coverart.Enricher) {
t.Helper() t.Helper()
h, pool := testHandlers(t) h, pool := testHandlers(t)
settings, err := coverart.NewSettingsService(t.Context(), pool, slog.Default()) settings, err := coverart.NewSettingsService(context.Background(), pool, slog.Default())
if err != nil { if err != nil {
t.Fatalf("NewSettingsService: %v", err) t.Fatalf("NewSettingsService: %v", err)
} }
+6 -6
View File
@@ -24,7 +24,7 @@ import (
) )
func TestHealthz(t *testing.T) { func TestHealthz(t *testing.T) {
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, library.RunScanConfig{}) s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
ts := httptest.NewServer(s.Router()) ts := httptest.NewServer(s.Router())
defer ts.Close() defer ts.Close()
@@ -73,7 +73,7 @@ func TestHealthz_IncludesMinClientVersion(t *testing.T) {
} }
func TestRouter_ServesSPAAtRoot(t *testing.T) { func TestRouter_ServesSPAAtRoot(t *testing.T) {
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, library.RunScanConfig{}) s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
ts := httptest.NewServer(s.Router()) ts := httptest.NewServer(s.Router())
defer ts.Close() defer ts.Close()
@@ -92,7 +92,7 @@ func TestRouter_ServesSPAAtRoot(t *testing.T) {
} }
func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) { func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) {
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, library.RunScanConfig{}) s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
ts := httptest.NewServer(s.Router()) ts := httptest.NewServer(s.Router())
defer ts.Close() defer ts.Close()
@@ -111,7 +111,7 @@ func TestRouter_DeepLinkFallbackReturnsSPA(t *testing.T) {
} }
func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) { func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) {
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, library.RunScanConfig{}) s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
ts := httptest.NewServer(s.Router()) ts := httptest.NewServer(s.Router())
defer ts.Close() defer ts.Close()
@@ -130,7 +130,7 @@ func TestRouter_APIPathNotSwallowedBySPA(t *testing.T) {
} }
func TestRouter_RestPathNotSwallowedBySPA(t *testing.T) { func TestRouter_RestPathNotSwallowedBySPA(t *testing.T) {
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, library.RunScanConfig{}) s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), nil, nil, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
ts := httptest.NewServer(s.Router()) ts := httptest.NewServer(s.Router())
defer ts.Close() defer ts.Close()
@@ -206,7 +206,7 @@ func TestRouter_AdminSubtreeNotShadowed(t *testing.T) {
} }
s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), pool, s := New(slog.New(slog.NewTextHandler(io.Discard, nil)), pool,
stubScanner{}, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, library.RunScanConfig{}) stubScanner{}, subsonic.Config{}, config.EventsConfig{}, config.RecommendationConfig{}, "", config.BrandingConfig{}, nil, 0, nil, nil, library.RunScanConfig{})
ts := httptest.NewServer(s.Router()) ts := httptest.NewServer(s.Router())
defer ts.Close() defer ts.Close()
@@ -1,5 +1,5 @@
import { afterEach, describe, expect, test, vi } from 'vitest'; import { afterEach, describe, expect, test, vi } from 'vitest';
import { render, screen, fireEvent, waitFor } from '@testing-library/svelte'; import { render, screen, fireEvent, waitFor, within } from '@testing-library/svelte';
import { mockQuery } from '../../../test-utils/query'; import { mockQuery } from '../../../test-utils/query';
import type { LidarrConfig } from '$lib/api/types'; import type { LidarrConfig } from '$lib/api/types';
import type { CoverProvider } from '$lib/api/admin'; import type { CoverProvider } from '$lib/api/admin';
@@ -133,11 +133,30 @@ function modalConfirmButton(): HTMLButtonElement {
return inDialog as HTMLButtonElement; return inDialog as HTMLButtonElement;
} }
// Section-scoping helpers. Both Lidarr and the cover-art-providers panel render
// "Save changes", "Test connection", and "API key" controls, so unscoped queries
// match multiple elements. These helpers narrow every query to its own <section>.
function lidarrSection(): HTMLElement {
return screen.getByRole('heading', { name: /^lidarr$/i, level: 3 }).closest('section') as HTMLElement;
}
function coverProvidersSection(): HTMLElement {
return screen.getByRole('heading', { name: /cover art providers/i, level: 3 }).closest('section') as HTMLElement;
}
// Each provider is rendered inside an <article>. Provider headings are h4.
function providerCard(providerName: string): HTMLElement {
return within(coverProvidersSection())
.getByRole('heading', { name: providerName, level: 4 })
.closest('article') as HTMLElement;
}
describe('/admin/integrations', () => { describe('/admin/integrations', () => {
test('Save calls putLidarrConfig with empty api_key when input is blank', async () => { test('Save calls putLidarrConfig with empty api_key when input is blank', async () => {
setup(); setup();
(putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected); (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected);
await fireEvent.click(screen.getByRole('button', { name: /save changes/i })); await fireEvent.click(within(lidarrSection()).getByRole('button', { name: /save changes/i }));
expect(putLidarrConfig).toHaveBeenCalledWith( expect(putLidarrConfig).toHaveBeenCalledWith(
expect.objectContaining({ expect.objectContaining({
enabled: true, enabled: true,
@@ -154,7 +173,7 @@ describe('/admin/integrations', () => {
const apiInput = screen.getByPlaceholderText(/saved — leave empty to keep/i); const apiInput = screen.getByPlaceholderText(/saved — leave empty to keep/i);
await fireEvent.input(apiInput, { target: { value: 'newkey' } }); await fireEvent.input(apiInput, { target: { value: 'newkey' } });
(putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected); (putLidarrConfig as ReturnType<typeof vi.fn>).mockResolvedValueOnce(cfgConnected);
await fireEvent.click(screen.getByRole('button', { name: /save changes/i })); await fireEvent.click(within(lidarrSection()).getByRole('button', { name: /save changes/i }));
expect(putLidarrConfig).toHaveBeenCalledWith( expect(putLidarrConfig).toHaveBeenCalledWith(
expect.objectContaining({ api_key: 'newkey' }) expect.objectContaining({ api_key: 'newkey' })
); );
@@ -167,7 +186,7 @@ describe('/admin/integrations', () => {
version: '2.0.0' version: '2.0.0'
}); });
await fireEvent.click( await fireEvent.click(
screen.getByRole('button', { name: /test connection/i }) within(lidarrSection()).getByRole('button', { name: /test connection/i })
); );
await waitFor(() => await waitFor(() =>
expect(screen.getByText(/lidarr 2\.0\.0/i)).toBeInTheDocument() expect(screen.getByText(/lidarr 2\.0\.0/i)).toBeInTheDocument()
@@ -181,7 +200,7 @@ describe('/admin/integrations', () => {
error: 'lidarr_unreachable' error: 'lidarr_unreachable'
}); });
await fireEvent.click( await fireEvent.click(
screen.getByRole('button', { name: /test connection/i }) within(lidarrSection()).getByRole('button', { name: /test connection/i })
); );
await waitFor(() => await waitFor(() =>
expect(screen.getByText(/lidarr_unreachable/i)).toBeInTheDocument() expect(screen.getByText(/lidarr_unreachable/i)).toBeInTheDocument()
@@ -274,7 +293,8 @@ describe('/admin/integrations — cover art providers', () => {
test('Save button is disabled initially (no changes)', () => { test('Save button is disabled initially (no changes)', () => {
setup(); setup();
const saveButtons = screen.getAllByRole('button', { name: /save changes/i }); const cpSection = coverProvidersSection();
const saveButtons = within(cpSection).getAllByRole('button', { name: /save changes/i });
// Both provider cards render Save buttons, both should be disabled initially // Both provider cards render Save buttons, both should be disabled initially
for (const btn of saveButtons) { for (const btn of saveButtons) {
expect((btn as HTMLButtonElement).disabled).toBe(true); expect((btn as HTMLButtonElement).disabled).toBe(true);
@@ -283,11 +303,11 @@ describe('/admin/integrations — cover art providers', () => {
test('Save button enables after typing in API key field', async () => { test('Save button enables after typing in API key field', async () => {
setup(); setup();
const keyInput = screen.getByLabelText(/api key/i); const theAudioDbCard = providerCard('TheAudioDB');
const keyInput = within(theAudioDbCard).getByLabelText(/api key/i);
await fireEvent.input(keyInput, { target: { value: 'myapikey' } }); await fireEvent.input(keyInput, { target: { value: 'myapikey' } });
// The TheAudioDB save button should now be enabled // The TheAudioDB save button should now be enabled
const saveButtons = screen.getAllByRole('button', { name: /save changes/i }); const theAudioDbSave = within(theAudioDbCard).getByRole('button', { name: /save changes/i });
const theAudioDbSave = saveButtons.find((b) => !b.closest('article')?.textContent?.includes('MusicBrainz'));
expect(theAudioDbSave).toBeDefined(); expect(theAudioDbSave).toBeDefined();
expect((theAudioDbSave as HTMLButtonElement).disabled).toBe(false); expect((theAudioDbSave as HTMLButtonElement).disabled).toBe(false);
}); });
@@ -298,14 +318,11 @@ describe('/admin/integrations — cover art providers', () => {
...providerTheAudioDB, ...providerTheAudioDB,
version_bumped: false version_bumped: false
}); });
const keyInput = screen.getByLabelText(/api key/i); const theAudioDbCard = providerCard('TheAudioDB');
const keyInput = within(theAudioDbCard).getByLabelText(/api key/i);
await fireEvent.input(keyInput, { target: { value: 'newkey123' } }); await fireEvent.input(keyInput, { target: { value: 'newkey123' } });
const saveButtons = screen.getAllByRole('button', { name: /save changes/i }); const btn = within(theAudioDbCard).getByRole('button', { name: /save changes/i });
const theAudioDbCard = screen.getByText('TheAudioDB').closest('article'); await fireEvent.click(btn);
const theAudioDbSave = theAudioDbCard?.querySelector('button[type="button"]');
// Find the save button for TheAudioDB specifically
const btn = saveButtons.find((b) => b.closest('article')?.contains(keyInput));
await fireEvent.click(btn!);
expect(updateCoverProvider).toHaveBeenCalledWith( expect(updateCoverProvider).toHaveBeenCalledWith(
'theaudiodb', 'theaudiodb',
expect.objectContaining({ api_key: 'newkey123' }) expect.objectContaining({ api_key: 'newkey123' })
@@ -364,8 +381,8 @@ describe('/admin/integrations — cover art providers', () => {
ok: true, ok: true,
duration_ms: 120 duration_ms: 120
}); });
const testButtons = screen.getAllByRole('button', { name: /test connection/i }); const mbcaaCard = providerCard('MusicBrainz CAA');
await fireEvent.click(testButtons[0]); await fireEvent.click(within(mbcaaCard).getByRole('button', { name: /test connection/i }));
await waitFor(() => await waitFor(() =>
expect(screen.getByText(/ok.*120ms/i)).toBeInTheDocument() expect(screen.getByText(/ok.*120ms/i)).toBeInTheDocument()
); );
@@ -377,8 +394,8 @@ describe('/admin/integrations — cover art providers', () => {
ok: false, ok: false,
error: 'mbcaa_unreachable' error: 'mbcaa_unreachable'
}); });
const testButtons = screen.getAllByRole('button', { name: /test connection/i }); const mbcaaCard = providerCard('MusicBrainz CAA');
await fireEvent.click(testButtons[0]); await fireEvent.click(within(mbcaaCard).getByRole('button', { name: /test connection/i }));
await waitFor(() => await waitFor(() =>
expect(screen.getByText(/mbcaa_unreachable/i)).toBeInTheDocument() expect(screen.getByText(/mbcaa_unreachable/i)).toBeInTheDocument()
); );