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:
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
);
|
);
|
||||||
|
|||||||
Reference in New Issue
Block a user