mirror of
https://github.com/arsvendg/Stirling-PDF.git
synced 2026-07-16 19:33:11 +02:00
Add auto-redact to V2 (#4417)
# Description of Changes Adds auto-redact tool to V2, with manual-redact in the UI but explicitly disabled. Also creates a shared component for the large buttons we're using in a couple different tools and uses consistently.
This commit is contained in:
@@ -0,0 +1,142 @@
|
||||
import { describe, expect, test, vi, beforeEach } from 'vitest';
|
||||
import { renderHook } from '@testing-library/react';
|
||||
import { buildRedactFormData, redactOperationConfig, useRedactOperation } from './useRedactOperation';
|
||||
import { defaultParameters, RedactParameters } from './useRedactParameters';
|
||||
|
||||
// Mock the useToolOperation hook
|
||||
vi.mock('../shared/useToolOperation', async () => {
|
||||
const actual = await vi.importActual('../shared/useToolOperation'); // Need to keep ToolType etc.
|
||||
return {
|
||||
...actual,
|
||||
useToolOperation: vi.fn()
|
||||
};
|
||||
});
|
||||
|
||||
// Mock the translation hook
|
||||
vi.mock('react-i18next', () => ({
|
||||
useTranslation: () => ({ t: vi.fn((_key: string, fallback: string) => fallback) })
|
||||
}));
|
||||
|
||||
// Mock the error handler utility
|
||||
vi.mock('../../../utils/toolErrorHandler', () => ({
|
||||
createStandardErrorHandler: vi.fn(() => vi.fn())
|
||||
}));
|
||||
|
||||
describe('buildRedactFormData', () => {
|
||||
const mockFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
|
||||
|
||||
test('should build form data for automatic mode', () => {
|
||||
const parameters: RedactParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'automatic',
|
||||
wordsToRedact: ['Confidential', 'Secret'],
|
||||
useRegex: true,
|
||||
wholeWordSearch: true,
|
||||
redactColor: '#FF0000',
|
||||
customPadding: 0.5,
|
||||
convertPDFToImage: false,
|
||||
};
|
||||
|
||||
const formData = buildRedactFormData(parameters, mockFile);
|
||||
|
||||
expect(formData.get('fileInput')).toBe(mockFile);
|
||||
expect(formData.get('listOfText')).toBe('Confidential\nSecret');
|
||||
expect(formData.get('useRegex')).toBe('true');
|
||||
expect(formData.get('wholeWordSearch')).toBe('true');
|
||||
expect(formData.get('redactColor')).toBe('FF0000'); // Hash should be removed
|
||||
expect(formData.get('customPadding')).toBe('0.5');
|
||||
expect(formData.get('convertPDFToImage')).toBe('false');
|
||||
});
|
||||
|
||||
test('should handle empty words array', () => {
|
||||
const parameters: RedactParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'automatic',
|
||||
wordsToRedact: [],
|
||||
};
|
||||
|
||||
const formData = buildRedactFormData(parameters, mockFile);
|
||||
|
||||
expect(formData.get('listOfText')).toBe('');
|
||||
});
|
||||
|
||||
test('should join multiple words with newlines', () => {
|
||||
const parameters: RedactParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'automatic',
|
||||
wordsToRedact: ['Word1', 'Word2', 'Word3'],
|
||||
};
|
||||
|
||||
const formData = buildRedactFormData(parameters, mockFile);
|
||||
|
||||
expect(formData.get('listOfText')).toBe('Word1\nWord2\nWord3');
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ description: 'remove hash from redact color', redactColor: '#123456', expected: '123456' },
|
||||
{ description: 'handle redact color without hash', redactColor: 'ABCDEF', expected: 'ABCDEF' },
|
||||
])('should $description', ({ redactColor, expected }) => {
|
||||
const parameters: RedactParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'automatic',
|
||||
redactColor,
|
||||
};
|
||||
|
||||
const formData = buildRedactFormData(parameters, mockFile);
|
||||
|
||||
expect(formData.get('redactColor')).toBe(expected);
|
||||
});
|
||||
|
||||
test('should convert boolean parameters to strings', () => {
|
||||
const parameters: RedactParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'automatic',
|
||||
useRegex: false,
|
||||
wholeWordSearch: true,
|
||||
convertPDFToImage: false,
|
||||
};
|
||||
|
||||
const formData = buildRedactFormData(parameters, mockFile);
|
||||
|
||||
expect(formData.get('useRegex')).toBe('false');
|
||||
expect(formData.get('wholeWordSearch')).toBe('true');
|
||||
expect(formData.get('convertPDFToImage')).toBe('false');
|
||||
});
|
||||
|
||||
test('should throw error for manual mode (not implemented)', () => {
|
||||
const parameters: RedactParameters = {
|
||||
...defaultParameters,
|
||||
mode: 'manual',
|
||||
};
|
||||
|
||||
expect(() => buildRedactFormData(parameters, mockFile)).toThrow('Manual redaction not yet implemented');
|
||||
});
|
||||
});
|
||||
|
||||
describe('useRedactOperation', () => {
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
test('should call useToolOperation with correct configuration', async () => {
|
||||
const { useToolOperation } = await import('../shared/useToolOperation');
|
||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||
|
||||
renderHook(() => useRedactOperation());
|
||||
|
||||
expect(mockUseToolOperation).toHaveBeenCalledWith({
|
||||
...redactOperationConfig,
|
||||
getErrorMessage: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
test('should provide error handler to useToolOperation', async () => {
|
||||
const { useToolOperation } = await import('../shared/useToolOperation');
|
||||
const mockUseToolOperation = vi.mocked(useToolOperation);
|
||||
|
||||
renderHook(() => useRedactOperation());
|
||||
|
||||
const callArgs = mockUseToolOperation.mock.calls[0][0];
|
||||
expect(typeof callArgs.getErrorMessage).toBe('function');
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,51 @@
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { useToolOperation, ToolType } from '../shared/useToolOperation';
|
||||
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
|
||||
import { RedactParameters, defaultParameters } from './useRedactParameters';
|
||||
|
||||
// Static configuration that can be used by both the hook and automation executor
|
||||
export const buildRedactFormData = (parameters: RedactParameters, file: File): FormData => {
|
||||
const formData = new FormData();
|
||||
formData.append("fileInput", file);
|
||||
|
||||
if (parameters.mode === 'automatic') {
|
||||
// Convert array to newline-separated string as expected by backend
|
||||
formData.append("listOfText", parameters.wordsToRedact.join('\n'));
|
||||
formData.append("useRegex", parameters.useRegex.toString());
|
||||
formData.append("wholeWordSearch", parameters.wholeWordSearch.toString());
|
||||
formData.append("redactColor", parameters.redactColor.replace('#', ''));
|
||||
formData.append("customPadding", parameters.customPadding.toString());
|
||||
formData.append("convertPDFToImage", parameters.convertPDFToImage.toString());
|
||||
} else {
|
||||
// Manual mode parameters would go here when implemented
|
||||
throw new Error('Manual redaction not yet implemented');
|
||||
}
|
||||
|
||||
return formData;
|
||||
};
|
||||
|
||||
// Static configuration object
|
||||
export const redactOperationConfig = {
|
||||
toolType: ToolType.singleFile,
|
||||
buildFormData: buildRedactFormData,
|
||||
operationType: 'redact',
|
||||
endpoint: (parameters: RedactParameters) => {
|
||||
if (parameters.mode === 'automatic') {
|
||||
return '/api/v1/security/auto-redact';
|
||||
} else {
|
||||
// Manual redaction endpoint would go here when implemented
|
||||
throw new Error('Manual redaction not yet implemented');
|
||||
}
|
||||
},
|
||||
filePrefix: 'redacted_',
|
||||
defaultParameters,
|
||||
} as const;
|
||||
|
||||
export const useRedactOperation = () => {
|
||||
const { t } = useTranslation();
|
||||
|
||||
return useToolOperation<RedactParameters>({
|
||||
...redactOperationConfig,
|
||||
getErrorMessage: createStandardErrorHandler(t('redact.error.failed', 'An error occurred while redacting the PDF.'))
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,134 @@
|
||||
import { describe, expect, test } from 'vitest';
|
||||
import { renderHook, act } from '@testing-library/react';
|
||||
import { useRedactParameters, defaultParameters } from './useRedactParameters';
|
||||
|
||||
describe('useRedactParameters', () => {
|
||||
test('should initialize with default parameters', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||
});
|
||||
|
||||
test.each([
|
||||
{ paramName: 'mode' as const, value: 'manual' as const },
|
||||
{ paramName: 'wordsToRedact' as const, value: ['word1', 'word2'] },
|
||||
{ paramName: 'useRegex' as const, value: true },
|
||||
{ paramName: 'wholeWordSearch' as const, value: true },
|
||||
{ paramName: 'redactColor' as const, value: '#FF0000' },
|
||||
{ paramName: 'customPadding' as const, value: 0.5 },
|
||||
{ paramName: 'convertPDFToImage' as const, value: false }
|
||||
])('should update parameter $paramName', ({ paramName, value }) => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter(paramName, value);
|
||||
});
|
||||
|
||||
expect(result.current.parameters[paramName]).toStrictEqual(value);
|
||||
});
|
||||
|
||||
test('should reset parameters to defaults', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
// Modify some parameters
|
||||
act(() => {
|
||||
result.current.updateParameter('mode', 'manual');
|
||||
result.current.updateParameter('wordsToRedact', ['test']);
|
||||
result.current.updateParameter('useRegex', true);
|
||||
});
|
||||
|
||||
// Reset parameters
|
||||
act(() => {
|
||||
result.current.resetParameters();
|
||||
});
|
||||
|
||||
expect(result.current.parameters).toStrictEqual(defaultParameters);
|
||||
});
|
||||
|
||||
describe('validation', () => {
|
||||
test.each([
|
||||
{ description: 'validate when wordsToRedact has non-empty words in automatic mode', wordsToRedact: ['word1', 'word2'], expected: true },
|
||||
{ description: 'not validate when wordsToRedact is empty in automatic mode', wordsToRedact: [], expected: false },
|
||||
{ description: 'not validate when wordsToRedact contains only empty strings in automatic mode', wordsToRedact: ['', ' ', ''], expected: false },
|
||||
{ description: 'validate when wordsToRedact contains at least one non-empty word in automatic mode', wordsToRedact: ['', 'valid', ' '], expected: true },
|
||||
])('should $description', ({ wordsToRedact, expected }) => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('mode', 'automatic');
|
||||
result.current.updateParameter('wordsToRedact', wordsToRedact);
|
||||
});
|
||||
|
||||
expect(result.current.validateParameters()).toBe(expected);
|
||||
});
|
||||
|
||||
test('should not validate in manual mode (not implemented)', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('mode', 'manual');
|
||||
});
|
||||
|
||||
expect(result.current.validateParameters()).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('endpoint handling', () => {
|
||||
test('should return correct endpoint for automatic mode', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('mode', 'automatic');
|
||||
});
|
||||
|
||||
expect(result.current.getEndpointName()).toBe('/api/v1/security/auto-redact');
|
||||
});
|
||||
|
||||
test('should throw error for manual mode (not implemented)', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('mode', 'manual');
|
||||
});
|
||||
|
||||
expect(() => result.current.getEndpointName()).toThrow('Manual redaction not yet implemented');
|
||||
});
|
||||
});
|
||||
|
||||
test('should maintain parameter state across updates', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('redactColor', '#FF0000');
|
||||
result.current.updateParameter('customPadding', 0.5);
|
||||
result.current.updateParameter('wordsToRedact', ['word1']);
|
||||
});
|
||||
|
||||
// All parameters should be updated
|
||||
expect(result.current.parameters.redactColor).toBe('#FF0000');
|
||||
expect(result.current.parameters.customPadding).toBe(0.5);
|
||||
expect(result.current.parameters.wordsToRedact).toEqual(['word1']);
|
||||
|
||||
// Other parameters should remain at defaults
|
||||
expect(result.current.parameters.mode).toBe('automatic');
|
||||
expect(result.current.parameters.useRegex).toBe(false);
|
||||
expect(result.current.parameters.wholeWordSearch).toBe(false);
|
||||
expect(result.current.parameters.convertPDFToImage).toBe(true);
|
||||
});
|
||||
|
||||
test('should handle array parameter updates correctly', () => {
|
||||
const { result } = renderHook(() => useRedactParameters());
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('wordsToRedact', ['initial']);
|
||||
});
|
||||
|
||||
expect(result.current.parameters.wordsToRedact).toEqual(['initial']);
|
||||
|
||||
act(() => {
|
||||
result.current.updateParameter('wordsToRedact', ['updated', 'multiple']);
|
||||
});
|
||||
|
||||
expect(result.current.parameters.wordsToRedact).toEqual(['updated', 'multiple']);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import { BaseParameters } from '../../../types/parameters';
|
||||
import { useBaseParameters, BaseParametersHook } from '../shared/useBaseParameters';
|
||||
|
||||
export type RedactMode = 'automatic' | 'manual';
|
||||
|
||||
export interface RedactParameters extends BaseParameters {
|
||||
mode: RedactMode;
|
||||
|
||||
// Automatic redaction parameters
|
||||
wordsToRedact: string[];
|
||||
useRegex: boolean;
|
||||
wholeWordSearch: boolean;
|
||||
redactColor: string;
|
||||
customPadding: number;
|
||||
convertPDFToImage: boolean;
|
||||
}
|
||||
|
||||
export const defaultParameters: RedactParameters = {
|
||||
mode: 'automatic',
|
||||
wordsToRedact: [],
|
||||
useRegex: false,
|
||||
wholeWordSearch: false,
|
||||
redactColor: '#000000',
|
||||
customPadding: 0.1,
|
||||
convertPDFToImage: true,
|
||||
};
|
||||
|
||||
export type RedactParametersHook = BaseParametersHook<RedactParameters>;
|
||||
|
||||
export const useRedactParameters = (): RedactParametersHook => {
|
||||
return useBaseParameters({
|
||||
defaultParameters,
|
||||
endpointName: (params) => {
|
||||
if (params.mode === 'automatic') {
|
||||
return '/api/v1/security/auto-redact';
|
||||
}
|
||||
// Manual redaction endpoint would go here when implemented
|
||||
throw new Error('Manual redaction not yet implemented');
|
||||
},
|
||||
validateFn: (params) => {
|
||||
if (params.mode === 'automatic') {
|
||||
return params.wordsToRedact.length > 0 && params.wordsToRedact.some(word => word.trim().length > 0);
|
||||
}
|
||||
// Manual mode validation would go here when implemented
|
||||
return false;
|
||||
}
|
||||
});
|
||||
};
|
||||
Reference in New Issue
Block a user