From 25a0252b09f43ccbdd8354222495f1aaa2279692 Mon Sep 17 00:00:00 2001 From: James Date: Tue, 5 Aug 2025 17:22:03 +0100 Subject: [PATCH] Add tests for sanitize --- .../tools/sanitize/SanitizeSettings.test.tsx | 194 ++++++++++++++ .../sanitize/useSanitizeOperation.test.ts | 236 ++++++++++++++++++ .../sanitize/useSanitizeParameters.test.ts | 110 ++++++++ 3 files changed, 540 insertions(+) create mode 100644 frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx create mode 100644 frontend/src/hooks/tools/sanitize/useSanitizeOperation.test.ts create mode 100644 frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts diff --git a/frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx b/frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx new file mode 100644 index 000000000..cf8e0c487 --- /dev/null +++ b/frontend/src/components/tools/sanitize/SanitizeSettings.test.tsx @@ -0,0 +1,194 @@ +import { describe, expect, test, vi, beforeEach } from 'vitest'; +import { render, screen, fireEvent } from '@testing-library/react'; +import { MantineProvider } from '@mantine/core'; +import SanitizeSettings from './SanitizeSettings'; +import { SanitizeParameters } from '../../../hooks/tools/sanitize/useSanitizeParameters'; + +// Mock useTranslation with predictable return values +const mockT = vi.fn((key: string) => `mock-${key}`); +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ t: mockT }) +})); + +// Wrapper component to provide Mantine context +const TestWrapper = ({ children }: { children: React.ReactNode }) => ( + {children} +); + +describe('SanitizeSettings', () => { + const defaultParameters: SanitizeParameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false, + }; + + const mockOnParameterChange = vi.fn(); + + beforeEach(() => { + vi.clearAllMocks(); + }); + + test('should render all sanitization option checkboxes', () => { + render( + + + + ); + + // Should render one checkbox for each parameter + const expectedCheckboxCount = Object.keys(defaultParameters).length; + const checkboxes = screen.getAllByRole('checkbox'); + expect(checkboxes).toHaveLength(expectedCheckboxCount); + }); + + test('should show correct initial checkbox states based on parameters', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + const parameterValues = Object.values(defaultParameters); + + parameterValues.forEach((value, index) => { + if (value) { + expect(checkboxes[index]).toBeChecked(); + } else { + expect(checkboxes[index]).not.toBeChecked(); + } + }); + }); + + test('should call onParameterChange with correct parameters when checkboxes are clicked', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + + // Click the first checkbox (removeJavaScript - should toggle from true to false) + fireEvent.click(checkboxes[0]); + expect(mockOnParameterChange).toHaveBeenCalledWith('removeJavaScript', false); + + // Click the third checkbox (removeXMPMetadata - should toggle from false to true) + fireEvent.click(checkboxes[2]); + expect(mockOnParameterChange).toHaveBeenCalledWith('removeXMPMetadata', true); + }); + + test('should disable all checkboxes when disabled prop is true', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + checkboxes.forEach(checkbox => { + expect(checkbox).toBeDisabled(); + }); + }); + + test('should enable all checkboxes when disabled prop is false or undefined', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + checkboxes.forEach(checkbox => { + expect(checkbox).not.toBeDisabled(); + }); + }); + + test('should handle different parameter combinations', () => { + const allEnabledParameters: SanitizeParameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: true, + removeMetadata: true, + removeLinks: true, + removeFonts: true, + }; + + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + checkboxes.forEach(checkbox => { + expect(checkbox).toBeChecked(); + }); + }); + + test('should call translation function with correct keys', () => { + render( + + + + ); + + // Verify that translation keys are being called (just check that it was called, not specific order) + expect(mockT).toHaveBeenCalledWith('sanitize.options.title', 'Sanitization Options'); + expect(mockT).toHaveBeenCalledWith('sanitize.options.removeJavaScript', 'Remove JavaScript'); + expect(mockT).toHaveBeenCalledWith('sanitize.options.removeEmbeddedFiles', 'Remove Embedded Files'); + expect(mockT).toHaveBeenCalledWith('sanitize.options.note', expect.any(String)); + }); + + test('should not call onParameterChange when disabled', () => { + render( + + + + ); + + const checkboxes = screen.getAllByRole('checkbox'); + + // Verify checkboxes are disabled + checkboxes.forEach(checkbox => { + expect(checkbox).toBeDisabled(); + }); + + // Try to click a disabled checkbox - this might still fire the event in tests + // but we can verify the checkbox state doesn't actually change + const firstCheckbox = checkboxes[0] as HTMLInputElement; + const initialChecked = firstCheckbox.checked; + fireEvent.click(firstCheckbox); + expect(firstCheckbox.checked).toBe(initialChecked); + }); +}); \ No newline at end of file diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeOperation.test.ts b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.test.ts new file mode 100644 index 000000000..d95fcb854 --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeOperation.test.ts @@ -0,0 +1,236 @@ +import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useSanitizeOperation } from './useSanitizeOperation'; + +// Mock useTranslation +vi.mock('react-i18next', () => ({ + useTranslation: () => ({ + t: (key: string, fallback?: string, options?: any) => { + if (key === 'sanitize.error' && options?.error) { + return `Sanitization failed: ${options.error}`; + } + if (key === 'error.noFilesSelected') { + return 'No files selected'; + } + if (key === 'sanitize.error.generic') { + return 'Sanitization failed'; + } + return fallback || key; + } + }) +})); + +// Mock fetch +const mockFetch = vi.fn(); +globalThis.fetch = mockFetch; + +// Mock URL.createObjectURL and revokeObjectURL +globalThis.URL.createObjectURL = vi.fn(() => 'mock-blob-url'); +globalThis.URL.revokeObjectURL = vi.fn(); + +describe('useSanitizeOperation', () => { + beforeEach(() => { + vi.clearAllMocks(); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + test('should initialize with default state', () => { + const { result } = renderHook(() => useSanitizeOperation()); + + expect(result.current.isLoading).toBe(false); + expect(result.current.errorMessage).toBe(null); + expect(result.current.downloadUrl).toBe(null); + expect(result.current.status).toBe(null); + }); + + test('should execute sanitization operation successfully', async () => { + const mockBlob = new Blob(['test'], { type: 'application/pdf' }); + const mockResponse = { + ok: true, + blob: () => Promise.resolve(mockBlob) + }; + mockFetch.mockResolvedValueOnce(mockResponse); + + const { result } = renderHook(() => useSanitizeOperation()); + + const parameters = { + removeJavaScript: true, + removeEmbeddedFiles: false, + removeXMPMetadata: true, + removeMetadata: false, + removeLinks: false, + removeFonts: false + }; + + const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + + await act(async () => { + await result.current.executeOperation(parameters, [testFile]); + }); + + expect(mockFetch).toHaveBeenCalledWith('/api/v1/security/sanitize-pdf', { + method: 'POST', + body: expect.any(FormData) + }); + + expect(result.current.isLoading).toBe(false); + expect(result.current.downloadUrl).toBe('mock-blob-url'); + expect(result.current.status).toBe('Sanitization completed successfully'); + expect(result.current.errorMessage).toBe(null); + }); + + test('should handle API errors correctly', async () => { + const mockResponse = { + ok: false, + text: () => Promise.resolve('Server error') + }; + mockFetch.mockResolvedValueOnce(mockResponse); + + const { result } = renderHook(() => useSanitizeOperation()); + + const parameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false + }; + + const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + + await act(async () => { + try { + await result.current.executeOperation(parameters, [testFile]); + } catch (error) { + // Expected to throw + } + }); + + expect(result.current.isLoading).toBe(false); + expect(result.current.errorMessage).toBe('Sanitization failed: Server error'); + expect(result.current.downloadUrl).toBe(null); + expect(result.current.status).toBe(null); + }); + + test('should handle no files selected error', async () => { + const { result } = renderHook(() => useSanitizeOperation()); + + const parameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false + }; + + let thrownError: Error | null = null; + await act(async () => { + try { + await result.current.executeOperation(parameters, []); + } catch (error) { + thrownError = error as Error; + } + }); + + // The error should be thrown + expect(thrownError).toBeInstanceOf(Error); + expect(thrownError!.message).toBe('No files selected'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + test('should send correct form data to API', async () => { + const mockBlob = new Blob(['test'], { type: 'application/pdf' }); + const mockResponse = { + ok: true, + blob: () => Promise.resolve(mockBlob) + }; + mockFetch.mockResolvedValueOnce(mockResponse); + + const { result } = renderHook(() => useSanitizeOperation()); + + const parameters = { + removeJavaScript: true, + removeEmbeddedFiles: false, + removeXMPMetadata: true, + removeMetadata: false, + removeLinks: true, + removeFonts: false + }; + + const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + + await act(async () => { + await result.current.executeOperation(parameters, [testFile]); + }); + + const [url, options] = mockFetch.mock.calls[0]; + expect(url).toBe('/api/v1/security/sanitize-pdf'); + expect(options.method).toBe('POST'); + + const formData = options.body as FormData; + expect(formData.get('removeJavaScript')).toBe('true'); + expect(formData.get('removeEmbeddedFiles')).toBe('false'); + expect(formData.get('removeXMPMetadata')).toBe('true'); + expect(formData.get('removeMetadata')).toBe('false'); + expect(formData.get('removeLinks')).toBe('true'); + expect(formData.get('removeFonts')).toBe('false'); + expect(formData.get('fileInput')).toBe(testFile); + }); + + test('should reset results correctly', () => { + const { result } = renderHook(() => useSanitizeOperation()); + + act(() => { + result.current.resetResults(); + }); + + expect(result.current.downloadUrl).toBe(null); + expect(result.current.errorMessage).toBe(null); + expect(result.current.status).toBe(null); + expect(globalThis.URL.revokeObjectURL).not.toHaveBeenCalled(); // No URL to revoke initially + }); + + test('should clear error message', async () => { + // Mock a failed API response + const mockResponse = { + ok: false, + text: () => Promise.resolve('API Error') + }; + mockFetch.mockResolvedValueOnce(mockResponse); + + const { result } = renderHook(() => useSanitizeOperation()); + + const parameters = { + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false + }; + + const testFile = new File(['test'], 'test.pdf', { type: 'application/pdf' }); + + // Trigger an API error + await act(async () => { + try { + await result.current.executeOperation(parameters, [testFile]); + } catch (error) { + // Expected to throw + } + }); + + expect(result.current.errorMessage).toBeTruthy(); + + act(() => { + result.current.clearError(); + }); + + expect(result.current.errorMessage).toBe(null); + }); +}); diff --git a/frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts new file mode 100644 index 000000000..9d09ea8bf --- /dev/null +++ b/frontend/src/hooks/tools/sanitize/useSanitizeParameters.test.ts @@ -0,0 +1,110 @@ +import { describe, expect, test } from 'vitest'; +import { renderHook, act } from '@testing-library/react'; +import { useSanitizeParameters } from './useSanitizeParameters'; + +describe('useSanitizeParameters', () => { + test('should initialize with default parameters', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + expect(result.current.parameters).toEqual({ + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false, + }); + }); + + test('should update individual parameters', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + act(() => { + result.current.updateParameter('removeXMPMetadata', true); + }); + + expect(result.current.parameters.removeXMPMetadata).toBe(true); + expect(result.current.parameters.removeJavaScript).toBe(true); // Other params unchanged + expect(result.current.parameters.removeLinks).toBe(false); // Other params unchanged + }); + + test('should reset parameters to defaults', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + // First, change some parameters + act(() => { + result.current.updateParameter('removeXMPMetadata', true); + result.current.updateParameter('removeJavaScript', false); + }); + + expect(result.current.parameters.removeXMPMetadata).toBe(true); + expect(result.current.parameters.removeJavaScript).toBe(false); + + // Then reset + act(() => { + result.current.resetParameters(); + }); + + expect(result.current.parameters).toEqual({ + removeJavaScript: true, + removeEmbeddedFiles: true, + removeXMPMetadata: false, + removeMetadata: false, + removeLinks: false, + removeFonts: false, + }); + }); + + test('should return correct endpoint name', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + expect(result.current.getEndpointName()).toBe('sanitize-pdf'); + }); + + test('should validate parameters correctly', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + // Default state should be valid (has removeJavaScript and removeEmbeddedFiles enabled) + expect(result.current.validateParameters()).toBe(true); + + // Turn off all parameters - should be invalid + act(() => { + result.current.updateParameter('removeJavaScript', false); + result.current.updateParameter('removeEmbeddedFiles', false); + }); + + expect(result.current.validateParameters()).toBe(false); + + // Turn on one parameter - should be valid again + act(() => { + result.current.updateParameter('removeLinks', true); + }); + + expect(result.current.validateParameters()).toBe(true); + }); + + test('should handle all parameter types correctly', () => { + const { result } = renderHook(() => useSanitizeParameters()); + + const allParameters = [ + 'removeJavaScript', + 'removeEmbeddedFiles', + 'removeXMPMetadata', + 'removeMetadata', + 'removeLinks', + 'removeFonts' + ] as const; + + allParameters.forEach(param => { + act(() => { + result.current.updateParameter(param, true); + }); + expect(result.current.parameters[param]).toBe(true); + + act(() => { + result.current.updateParameter(param, false); + }); + expect(result.current.parameters[param]).toBe(false); + }); + }); +});