From 9f2c5c685bc9b70a7e0d295e1c72666499d9020b Mon Sep 17 00:00:00 2001 From: Antti Kettunen Date: Sun, 17 May 2026 19:39:11 +0300 Subject: [PATCH] refactor(import): simplify API failure handling - rely on ky for transport errors across import/staging calls - keep explicit soft-failure checks for auto-import approval endpoints - add regression test for approval/rejection soft failures --- webui/src/routes/import/-import.api.test.ts | 29 +++ webui/src/routes/import/-import.api.ts | 206 +++++++------------- 2 files changed, 103 insertions(+), 132 deletions(-) create mode 100644 webui/src/routes/import/-import.api.test.ts diff --git a/webui/src/routes/import/-import.api.test.ts b/webui/src/routes/import/-import.api.test.ts new file mode 100644 index 00000000..1963b1f7 --- /dev/null +++ b/webui/src/routes/import/-import.api.test.ts @@ -0,0 +1,29 @@ +import { describe, expect, it } from 'vitest'; + +import { HttpResponse, http, server } from '@/test/msw'; + +import { approveAutoImportResult, rejectAutoImportResult } from './-import.api'; + +const softFailureMessage = 'Item not found or not pending review'; + +describe('import api', () => { + it('surfaces soft failures from auto-import approval endpoints', async () => { + server.use( + http.post('/api/auto-import/approve/17', () => + HttpResponse.json({ + success: false, + error: softFailureMessage, + }), + ), + http.post('/api/auto-import/reject/18', () => + HttpResponse.json({ + success: false, + error: softFailureMessage, + }), + ), + ); + + await expect(approveAutoImportResult(17)).rejects.toThrow(softFailureMessage); + await expect(rejectAutoImportResult(18)).rejects.toThrow(softFailureMessage); + }); +}); diff --git a/webui/src/routes/import/-import.api.ts b/webui/src/routes/import/-import.api.ts index 624d8a36..8de7234d 100644 --- a/webui/src/routes/import/-import.api.ts +++ b/webui/src/routes/import/-import.api.ts @@ -18,48 +18,26 @@ import type { export const IMPORT_QUERY_KEY = ['import'] as const; -function assertSuccess( - payload: T, - fallback: string, -): T { - if (!payload.success) { - throw new Error(payload.error || fallback); - } - return payload; -} - export async function fetchImportStagingFiles(): Promise { - return assertSuccess( - await readJson(apiClient.get('import/staging/files')), - 'Failed to load import folder', - ); + return readJson(apiClient.get('import/staging/files')); } export async function fetchImportStagingGroups(): Promise { - return assertSuccess( - await readJson(apiClient.get('import/staging/groups')), - 'Failed to load auto-detected albums', - ); + return readJson(apiClient.get('import/staging/groups')); } export async function fetchImportStagingSuggestions(): Promise { - return assertSuccess( - await readJson(apiClient.get('import/staging/suggestions')), - 'Failed to load import suggestions', - ); + return readJson(apiClient.get('import/staging/suggestions')); } export async function searchImportAlbums(query: string): Promise { - return assertSuccess( - await readJson( - apiClient.get('import/search/albums', { - searchParams: { - q: query, - limit: '12', - }, - }), - ), - 'Album search failed', + return readJson( + apiClient.get('import/search/albums', { + searchParams: { + q: query, + limit: '12', + }, + }), ); } @@ -70,19 +48,16 @@ export async function matchImportAlbum(input: { albumArtist?: string | null; filePaths?: string[] | null; }): Promise { - return assertSuccess( - await readJson( - apiClient.post('import/album/match', { - json: { - album_id: input.albumId, - source: input.source || '', - album_name: input.albumName || '', - album_artist: input.albumArtist || '', - ...(input.filePaths?.length ? { file_paths: input.filePaths } : {}), - }, - }), - ), - 'Failed to match album', + return readJson( + apiClient.post('import/album/match', { + json: { + album_id: input.albumId, + source: input.source || '', + album_name: input.albumName || '', + album_artist: input.albumArtist || '', + ...(input.filePaths?.length ? { file_paths: input.filePaths } : {}), + }, + }), ); } @@ -90,142 +65,109 @@ export async function processImportAlbumTrack(input: { album: ImportAlbum; match: ImportAlbumMatch; }): Promise { - return assertSuccess( - await readJson( - apiClient.post('import/album/process', { - json: { - album: input.album, - matches: [input.match], - }, - }), - ), - 'Failed to process album track', + return readJson( + apiClient.post('import/album/process', { + json: { + album: input.album, + matches: [input.match], + }, + }), ); } export async function searchImportTracks(query: string): Promise { - return assertSuccess( - await readJson( - apiClient.get('import/search/tracks', { - searchParams: { - q: query, - limit: '6', - }, - }), - ), - 'Track search failed', + return readJson( + apiClient.get('import/search/tracks', { + searchParams: { + q: query, + limit: '6', + }, + }), ); } export async function processImportSingleFile(file: unknown): Promise { - return assertSuccess( - await readJson( - apiClient.post('import/singles/process', { - json: { - files: [file], - }, - }), - ), - 'Failed to process single', + return readJson( + apiClient.post('import/singles/process', { + json: { + files: [file], + }, + }), ); } export async function fetchAutoImportStatus(): Promise { - return assertSuccess( - await readJson(apiClient.get('auto-import/status')), - 'Failed to load auto-import status', - ); + return readJson(apiClient.get('auto-import/status')); } export async function fetchAutoImportSettings(): Promise { - return assertSuccess( - await readJson(apiClient.get('auto-import/settings')), - 'Failed to load auto-import settings', - ); + return readJson(apiClient.get('auto-import/settings')); } export async function saveAutoImportSettings(input: { confidenceThreshold: number; scanInterval: number; }): Promise { - assertSuccess( - await readJson<{ success: boolean; error?: string }>( - apiClient.post('auto-import/settings', { - json: { - confidence_threshold: input.confidenceThreshold, - scan_interval: input.scanInterval, - }, - }), - ), - 'Failed to save auto-import settings', + await readJson<{ success: boolean; error?: string }>( + apiClient.post('auto-import/settings', { + json: { + confidence_threshold: input.confidenceThreshold, + scan_interval: input.scanInterval, + }, + }), ); } export async function fetchAutoImportResults(): Promise { - return assertSuccess( - await readJson( - apiClient.get('auto-import/results', { - searchParams: { - limit: '100', - }, - }), - ), - 'Failed to load auto-import results', + return readJson( + apiClient.get('auto-import/results', { + searchParams: { + limit: '100', + }, + }), ); } export async function toggleAutoImport(enabled: boolean): Promise { - assertSuccess( - await readJson<{ success: boolean; error?: string }>( - apiClient.post('auto-import/toggle', { - json: { enabled }, - }), - ), - 'Failed to toggle auto-import', + await readJson<{ success: boolean; error?: string }>( + apiClient.post('auto-import/toggle', { + json: { enabled }, + }), ); } export async function triggerAutoImportScan(): Promise { - assertSuccess( - await readJson<{ success: boolean; error?: string }>(apiClient.post('auto-import/scan-now')), - 'Failed to trigger scan', - ); + await readJson<{ success: boolean; error?: string }>(apiClient.post('auto-import/scan-now')); } export async function approveAutoImportResult(id: number): Promise { - assertSuccess( - await readJson<{ success: boolean; error?: string }>( - apiClient.post(`auto-import/approve/${id}`), - ), - 'Failed to approve import', + const payload = await readJson<{ success: boolean; error?: string }>( + apiClient.post(`auto-import/approve/${id}`), ); + if (!payload.success) { + throw new Error(payload.error || 'Failed to approve import'); + } } export async function rejectAutoImportResult(id: number): Promise { - assertSuccess( - await readJson<{ success: boolean; error?: string }>( - apiClient.post(`auto-import/reject/${id}`), - ), - 'Failed to dismiss import', + const payload = await readJson<{ success: boolean; error?: string }>( + apiClient.post(`auto-import/reject/${id}`), ); + if (!payload.success) { + throw new Error(payload.error || 'Failed to dismiss import'); + } } export async function approveAllAutoImportResults(): Promise { - const payload = assertSuccess( - await readJson<{ success: boolean; count?: number; error?: string }>( - apiClient.post('auto-import/approve-all'), - ), - 'Failed to approve imports', + const payload = await readJson<{ success: boolean; count?: number; error?: string }>( + apiClient.post('auto-import/approve-all'), ); return payload.count ?? 0; } export async function clearCompletedAutoImportResults(): Promise { - const payload = assertSuccess( - await readJson<{ success: boolean; count?: number; error?: string }>( - apiClient.post('auto-import/clear-completed'), - ), - 'Failed to clear import history', + const payload = await readJson<{ success: boolean; count?: number; error?: string }>( + apiClient.post('auto-import/clear-completed'), ); return payload.count ?? 0; }