test(web/admin): cover Test-then-Save flow on /admin/integrations
Three new cases: - Save on a fresh form calls test first, fills auto-defaults from the response, then put-config with non-zero defaults - Save aborts when test fails and surfaces the error code - An operator-picked default value survives the test → save sequence (auto-default $effect only fills when current value is 0/'') Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -95,6 +95,17 @@ const defaultCoverProviders = {
|
|||||||
sources_version: 1
|
sources_version: 1
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const testResponseOk = {
|
||||||
|
ok: true as const,
|
||||||
|
version: '2.0.5',
|
||||||
|
quality_profiles: [
|
||||||
|
{ id: 1, name: 'FLAC' },
|
||||||
|
{ id: 2, name: 'MP3-320' }
|
||||||
|
],
|
||||||
|
metadata_profiles: [{ id: 1, name: 'Standard' }],
|
||||||
|
root_folders: [{ path: '/music', accessible: true, free_space: 12345 }]
|
||||||
|
};
|
||||||
|
|
||||||
const smtpUnset: SMTPConfig = {
|
const smtpUnset: SMTPConfig = {
|
||||||
enabled: false,
|
enabled: false,
|
||||||
host: '',
|
host: '',
|
||||||
@@ -509,3 +520,118 @@ describe('/admin/integrations — SMTP card', () => {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Lidarr first-time setup (Test-then-Save)', () => {
|
||||||
|
afterEach(() => {
|
||||||
|
vi.clearAllMocks();
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Save calls test first, fills defaults from response, then put-config', async () => {
|
||||||
|
vi.mocked(createLidarrConfigQuery).mockReturnValue(mockQuery(cfgUnset));
|
||||||
|
vi.mocked(createQualityProfilesQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createMetadataProfilesQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createRootFoldersQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createCoverProvidersQuery).mockReturnValue(mockQuery(defaultCoverProviders));
|
||||||
|
vi.mocked(createSMTPConfigQuery).mockReturnValue(mockQuery(smtpUnset));
|
||||||
|
vi.mocked(testLidarrConnection).mockResolvedValue(testResponseOk);
|
||||||
|
vi.mocked(putLidarrConfig).mockResolvedValue(cfgConnected);
|
||||||
|
|
||||||
|
render(IntegrationsPage);
|
||||||
|
|
||||||
|
await fireEvent.input(screen.getByLabelText(/Base URL/i), {
|
||||||
|
target: { value: 'http://lidarr.local' }
|
||||||
|
});
|
||||||
|
await fireEvent.input(screen.getByLabelText(/API key/i), {
|
||||||
|
target: { value: 'secret-key' }
|
||||||
|
});
|
||||||
|
await fireEvent.click(screen.getByRole('button', { name: /Save changes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(testLidarrConnection).toHaveBeenCalledWith({
|
||||||
|
base_url: 'http://lidarr.local',
|
||||||
|
api_key: 'secret-key'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(putLidarrConfig).toHaveBeenCalledWith({
|
||||||
|
enabled: true,
|
||||||
|
base_url: 'http://lidarr.local',
|
||||||
|
api_key: 'secret-key',
|
||||||
|
default_quality_profile_id: 1,
|
||||||
|
default_metadata_profile_id: 1,
|
||||||
|
default_root_folder_path: '/music'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Save aborts when test fails and surfaces the error', async () => {
|
||||||
|
vi.mocked(createLidarrConfigQuery).mockReturnValue(mockQuery(cfgUnset));
|
||||||
|
vi.mocked(createQualityProfilesQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createMetadataProfilesQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createRootFoldersQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createCoverProvidersQuery).mockReturnValue(mockQuery(defaultCoverProviders));
|
||||||
|
vi.mocked(createSMTPConfigQuery).mockReturnValue(mockQuery(smtpUnset));
|
||||||
|
vi.mocked(testLidarrConnection).mockResolvedValue({
|
||||||
|
ok: false,
|
||||||
|
error: 'lidarr_unreachable'
|
||||||
|
});
|
||||||
|
|
||||||
|
render(IntegrationsPage);
|
||||||
|
|
||||||
|
await fireEvent.input(screen.getByLabelText(/Base URL/i), {
|
||||||
|
target: { value: 'http://wrong.local' }
|
||||||
|
});
|
||||||
|
await fireEvent.input(screen.getByLabelText(/API key/i), {
|
||||||
|
target: { value: 'k' }
|
||||||
|
});
|
||||||
|
await fireEvent.click(screen.getByRole('button', { name: /Save changes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(testLidarrConnection).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
expect(putLidarrConfig).not.toHaveBeenCalled();
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(screen.getByText(/Save failed — lidarr_unreachable/i)).toBeInTheDocument();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
test('Save preserves operator-picked defaults over auto-defaults', async () => {
|
||||||
|
vi.mocked(createLidarrConfigQuery).mockReturnValue(mockQuery(cfgUnset));
|
||||||
|
vi.mocked(createQualityProfilesQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createMetadataProfilesQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createRootFoldersQuery).mockReturnValue(mockQuery([]));
|
||||||
|
vi.mocked(createCoverProvidersQuery).mockReturnValue(mockQuery(defaultCoverProviders));
|
||||||
|
vi.mocked(createSMTPConfigQuery).mockReturnValue(mockQuery(smtpUnset));
|
||||||
|
vi.mocked(testLidarrConnection).mockResolvedValue(testResponseOk);
|
||||||
|
vi.mocked(putLidarrConfig).mockResolvedValue(cfgConnected);
|
||||||
|
|
||||||
|
render(IntegrationsPage);
|
||||||
|
|
||||||
|
await fireEvent.input(screen.getByLabelText(/Base URL/i), {
|
||||||
|
target: { value: 'http://lidarr.local' }
|
||||||
|
});
|
||||||
|
await fireEvent.input(screen.getByLabelText(/API key/i), {
|
||||||
|
target: { value: 'secret-key' }
|
||||||
|
});
|
||||||
|
// Click Test first to populate the dropdowns, then change the
|
||||||
|
// quality profile selection before clicking Save.
|
||||||
|
await fireEvent.click(screen.getByRole('button', { name: /Test connection/i }));
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(testLidarrConnection).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
const qualitySelect = screen.getByLabelText(/Default quality profile/i);
|
||||||
|
await fireEvent.change(qualitySelect, { target: { value: '2' } });
|
||||||
|
|
||||||
|
await fireEvent.click(screen.getByRole('button', { name: /Save changes/i }));
|
||||||
|
|
||||||
|
await waitFor(() => {
|
||||||
|
expect(putLidarrConfig).toHaveBeenCalledWith(
|
||||||
|
expect.objectContaining({
|
||||||
|
default_quality_profile_id: 2,
|
||||||
|
default_metadata_profile_id: 1,
|
||||||
|
default_root_folder_path: '/music'
|
||||||
|
})
|
||||||
|
);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user