diff --git a/frontend/src/api/__tests__/import.test.ts b/frontend/src/api/__tests__/import.test.ts index 605a4d41..b8fa3078 100644 --- a/frontend/src/api/__tests__/import.test.ts +++ b/frontend/src/api/__tests__/import.test.ts @@ -6,12 +6,14 @@ vi.mock('../client', () => ({ default: { get: vi.fn(), post: vi.fn(), + delete: vi.fn(), }, })); describe('import API', () => { const mockedGet = vi.mocked(client.get); const mockedPost = vi.mocked(client.post); + const mockedDelete = vi.mocked(client.delete); beforeEach(() => { vi.clearAllMocks(); @@ -71,11 +73,16 @@ describe('import API', () => { expect(result).toEqual(mockResponse); }); - it('cancelImport posts cancel', async () => { - mockedPost.mockResolvedValue({}); + it('cancelImport deletes cancel with required session_uuid query', async () => { + const sessionUUID = 'uuid-cancel-123'; + mockedDelete.mockResolvedValue({}); - await cancelImport(); - expect(client.post).toHaveBeenCalledWith('/import/cancel'); + await cancelImport(sessionUUID); + expect(client.delete).toHaveBeenCalledWith('/import/cancel', { + params: { + session_uuid: sessionUUID, + }, + }); }); it('forwards commitImport errors', async () => { @@ -87,9 +94,9 @@ describe('import API', () => { it('forwards cancelImport errors', async () => { const error = new Error('cancel failed'); - mockedPost.mockRejectedValue(error); + mockedDelete.mockRejectedValue(error); - await expect(cancelImport()).rejects.toBe(error); + await expect(cancelImport('uuid-cancel-123')).rejects.toBe(error); }); it('getImportStatus gets status', async () => { diff --git a/frontend/src/api/__tests__/jsonImport.test.ts b/frontend/src/api/__tests__/jsonImport.test.ts new file mode 100644 index 00000000..355ff09a --- /dev/null +++ b/frontend/src/api/__tests__/jsonImport.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { cancelJSONImport } from '../jsonImport'; +import client from '../client'; + +vi.mock('../client', () => ({ + default: { + post: vi.fn(), + }, +})); + +describe('jsonImport API', () => { + const mockedPost = vi.mocked(client.post); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('cancelJSONImport posts cancel endpoint with required session_uuid body', async () => { + const sessionUUID = 'json-session-123'; + mockedPost.mockResolvedValue({}); + + await cancelJSONImport(sessionUUID); + + expect(client.post).toHaveBeenCalledWith('/import/json/cancel', { + session_uuid: sessionUUID, + }); + }); + + it('forwards cancelJSONImport errors', async () => { + const error = new Error('cancel failed'); + mockedPost.mockRejectedValue(error); + + await expect(cancelJSONImport('json-session-123')).rejects.toBe(error); + }); +}); diff --git a/frontend/src/api/__tests__/npmImport.test.ts b/frontend/src/api/__tests__/npmImport.test.ts new file mode 100644 index 00000000..5599d85d --- /dev/null +++ b/frontend/src/api/__tests__/npmImport.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { cancelNPMImport } from '../npmImport'; +import client from '../client'; + +vi.mock('../client', () => ({ + default: { + post: vi.fn(), + }, +})); + +describe('npmImport API', () => { + const mockedPost = vi.mocked(client.post); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + it('cancelNPMImport posts cancel endpoint with required session_uuid body', async () => { + const sessionUUID = 'npm-session-123'; + mockedPost.mockResolvedValue({}); + + await cancelNPMImport(sessionUUID); + + expect(client.post).toHaveBeenCalledWith('/import/npm/cancel', { + session_uuid: sessionUUID, + }); + }); + + it('forwards cancelNPMImport errors', async () => { + const error = new Error('cancel failed'); + mockedPost.mockRejectedValue(error); + + await expect(cancelNPMImport('npm-session-123')).rejects.toBe(error); + }); +}); diff --git a/frontend/src/api/import.ts b/frontend/src/api/import.ts index bda5d7d8..ec74b92e 100644 --- a/frontend/src/api/import.ts +++ b/frontend/src/api/import.ts @@ -110,10 +110,15 @@ export const commitImport = async ( /** * Cancels the current import session. + * @param sessionUUID - The import session UUID * @throws {AxiosError} If cancellation fails */ -export const cancelImport = async (): Promise => { - await client.post('/import/cancel'); +export const cancelImport = async (sessionUUID: string): Promise => { + await client.delete('/import/cancel', { + params: { + session_uuid: sessionUUID, + }, + }); }; /** diff --git a/frontend/src/api/jsonImport.ts b/frontend/src/api/jsonImport.ts index db7713d9..9744c7a6 100644 --- a/frontend/src/api/jsonImport.ts +++ b/frontend/src/api/jsonImport.ts @@ -83,8 +83,11 @@ export const commitJSONImport = async ( /** * Cancels the current JSON import session. + * @param sessionUuid - The import session UUID * @throws {AxiosError} If cancellation fails */ -export const cancelJSONImport = async (): Promise => { - await client.post('/import/json/cancel'); +export const cancelJSONImport = async (sessionUuid: string): Promise => { + await client.post('/import/json/cancel', { + session_uuid: sessionUuid, + }); }; diff --git a/frontend/src/api/npmImport.ts b/frontend/src/api/npmImport.ts index 5ccdadc2..eb7e1761 100644 --- a/frontend/src/api/npmImport.ts +++ b/frontend/src/api/npmImport.ts @@ -83,8 +83,11 @@ export const commitNPMImport = async ( /** * Cancels the current NPM import session. + * @param sessionUuid - The import session UUID * @throws {AxiosError} If cancellation fails */ -export const cancelNPMImport = async (): Promise => { - await client.post('/import/npm/cancel'); +export const cancelNPMImport = async (sessionUuid: string): Promise => { + await client.post('/import/npm/cancel', { + session_uuid: sessionUuid, + }); }; diff --git a/frontend/src/hooks/__tests__/useImport.test.tsx b/frontend/src/hooks/__tests__/useImport.test.tsx index 1eac0ff7..7d141ffb 100644 --- a/frontend/src/hooks/__tests__/useImport.test.tsx +++ b/frontend/src/hooks/__tests__/useImport.test.tsx @@ -208,7 +208,7 @@ describe('useImport', () => { await result.current.cancel() }) - expect(api.cancelImport).toHaveBeenCalled() + expect(api.cancelImport).toHaveBeenCalledWith('session-3') await waitFor(() => { expect(result.current.session).toBeNull() }) diff --git a/frontend/src/hooks/__tests__/useJSONImport.test.tsx b/frontend/src/hooks/__tests__/useJSONImport.test.tsx new file mode 100644 index 00000000..9e04f484 --- /dev/null +++ b/frontend/src/hooks/__tests__/useJSONImport.test.tsx @@ -0,0 +1,69 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook, act, waitFor } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import React from 'react' +import { useJSONImport } from '../useJSONImport' +import * as api from '../../api/jsonImport' + +vi.mock('../../api/jsonImport', () => ({ + uploadJSONExport: vi.fn(), + commitJSONImport: vi.fn(), + cancelJSONImport: vi.fn(), +})) + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ) +} + +describe('useJSONImport', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes active session UUID to cancelJSONImport', async () => { + const sessionId = 'json-session-123' + vi.mocked(api.uploadJSONExport).mockResolvedValue({ + session: { + id: sessionId, + state: 'reviewing', + source: 'json', + }, + preview: { + hosts: [], + conflicts: [], + errors: [], + }, + conflict_details: {}, + }) + vi.mocked(api.cancelJSONImport).mockResolvedValue(undefined) + + const { result } = renderHook(() => useJSONImport(), { wrapper: createWrapper() }) + + await act(async () => { + await result.current.upload('{}') + }) + + await waitFor(() => { + expect(result.current.sessionId).toBe(sessionId) + }) + + await act(async () => { + await result.current.cancel() + }) + + expect(api.cancelJSONImport).toHaveBeenCalledWith(sessionId) + + await waitFor(() => { + expect(result.current.sessionId).toBeNull() + }) + }) +}) diff --git a/frontend/src/hooks/__tests__/useNPMImport.test.tsx b/frontend/src/hooks/__tests__/useNPMImport.test.tsx new file mode 100644 index 00000000..ed760259 --- /dev/null +++ b/frontend/src/hooks/__tests__/useNPMImport.test.tsx @@ -0,0 +1,69 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest' +import { renderHook, act, waitFor } from '@testing-library/react' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import React from 'react' +import { useNPMImport } from '../useNPMImport' +import * as api from '../../api/npmImport' + +vi.mock('../../api/npmImport', () => ({ + uploadNPMExport: vi.fn(), + commitNPMImport: vi.fn(), + cancelNPMImport: vi.fn(), +})) + +const createWrapper = () => { + const queryClient = new QueryClient({ + defaultOptions: { + queries: { retry: false }, + mutations: { retry: false }, + }, + }) + + return ({ children }: { children: React.ReactNode }) => ( + {children} + ) +} + +describe('useNPMImport', () => { + beforeEach(() => { + vi.clearAllMocks() + }) + + it('passes active session UUID to cancelNPMImport', async () => { + const sessionId = 'npm-session-123' + vi.mocked(api.uploadNPMExport).mockResolvedValue({ + session: { + id: sessionId, + state: 'reviewing', + source: 'npm', + }, + preview: { + hosts: [], + conflicts: [], + errors: [], + }, + conflict_details: {}, + }) + vi.mocked(api.cancelNPMImport).mockResolvedValue(undefined) + + const { result } = renderHook(() => useNPMImport(), { wrapper: createWrapper() }) + + await act(async () => { + await result.current.upload('{}') + }) + + await waitFor(() => { + expect(result.current.sessionId).toBe(sessionId) + }) + + await act(async () => { + await result.current.cancel() + }) + + expect(api.cancelNPMImport).toHaveBeenCalledWith(sessionId) + + await waitFor(() => { + expect(result.current.sessionId).toBeNull() + }) + }) +}) diff --git a/frontend/src/hooks/useImport.ts b/frontend/src/hooks/useImport.ts index 154da827..36685fd8 100644 --- a/frontend/src/hooks/useImport.ts +++ b/frontend/src/hooks/useImport.ts @@ -77,7 +77,11 @@ export function useImport() { }); const cancelMutation = useMutation({ - mutationFn: () => cancelImport(), + mutationFn: () => { + const sessionId = uploadPreview?.session?.id || statusQuery.data?.session?.id; + if (!sessionId) throw new Error('No active session'); + return cancelImport(sessionId); + }, onSuccess: () => { // Clear upload preview and remove query cache setUploadPreview(null); diff --git a/frontend/src/hooks/useJSONImport.ts b/frontend/src/hooks/useJSONImport.ts index 96287b9b..da8e28c1 100644 --- a/frontend/src/hooks/useJSONImport.ts +++ b/frontend/src/hooks/useJSONImport.ts @@ -46,7 +46,10 @@ export function useJSONImport() { }); const cancelMutation = useMutation({ - mutationFn: cancelJSONImport, + mutationFn: () => { + if (!sessionId) throw new Error('No active session'); + return cancelJSONImport(sessionId); + }, onSuccess: () => { setPreview(null); setSessionId(null); diff --git a/frontend/src/hooks/useNPMImport.ts b/frontend/src/hooks/useNPMImport.ts index dc9211a8..b6718463 100644 --- a/frontend/src/hooks/useNPMImport.ts +++ b/frontend/src/hooks/useNPMImport.ts @@ -46,7 +46,10 @@ export function useNPMImport() { }); const cancelMutation = useMutation({ - mutationFn: cancelNPMImport, + mutationFn: () => { + if (!sessionId) throw new Error('No active session'); + return cancelNPMImport(sessionId); + }, onSuccess: () => { setPreview(null); setSessionId(null);