Add Remove Password UI into V2 (#4214)

# Description of Changes
- Add UI for Remove Password tool
- Fix more translation warnings that were being thrown in the console
- Add an encrypted PDF thumbnail and refactor thumbnail generation code
This commit is contained in:
James Brunton
2025-08-18 15:26:29 +01:00
committed by GitHub
parent 4c17c520d7
commit acbebd67a3
20 changed files with 806 additions and 163 deletions
@@ -49,25 +49,6 @@ describe('useAddPasswordOperation', () => {
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
});
test('should configure useToolOperation with correct parameters', () => {
renderHook(() => useAddPasswordOperation());
expect(mockUseToolOperation).toHaveBeenCalledWith({
operationType: 'addPassword',
endpoint: '/api/v1/security/add-password',
buildFormData: expect.any(Function),
filePrefix: 'translated-addPassword.filenamePrefix_',
multiFileEndpoint: false,
getErrorMessage: 'error-handler-function'
});
});
test('should return the result from useToolOperation', () => {
const { result } = renderHook(() => useAddPasswordOperation());
expect(result.current).toBe(mockToolOperationReturn);
});
test.each([
{
description: 'with all parameters filled',
@@ -48,25 +48,6 @@ describe('useChangePermissionsOperation', () => {
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
});
test('should configure useToolOperation with correct parameters', () => {
renderHook(() => useChangePermissionsOperation());
expect(mockUseToolOperation).toHaveBeenCalledWith({
operationType: 'changePermissions',
endpoint: '/api/v1/security/add-password',
buildFormData: expect.any(Function),
filePrefix: 'permissions_',
multiFileEndpoint: false,
getErrorMessage: 'error-handler-function'
});
});
test('should return the result from useToolOperation', () => {
const { result } = renderHook(() => useChangePermissionsOperation());
expect(result.current).toBe(mockToolOperationReturn);
});
test.each([
{
preventAssembly: false,
@@ -0,0 +1,104 @@
import { describe, expect, test, vi, beforeEach } from 'vitest';
import { renderHook } from '@testing-library/react';
import { useRemovePasswordOperation } from './useRemovePasswordOperation';
import type { RemovePasswordParameters } from './useRemovePasswordParameters';
// Mock the useToolOperation hook
vi.mock('../shared/useToolOperation', () => ({
useToolOperation: vi.fn()
}));
// Mock the translation hook
const mockT = vi.fn((key: string) => `translated-${key}`);
vi.mock('react-i18next', () => ({
useTranslation: () => ({ t: mockT })
}));
// Mock the error handler
vi.mock('../../../utils/toolErrorHandler', () => ({
createStandardErrorHandler: vi.fn(() => 'error-handler-function')
}));
// Import the mocked function
import { ToolOperationConfig, ToolOperationHook, useToolOperation } from '../shared/useToolOperation';
describe('useRemovePasswordOperation', () => {
const mockUseToolOperation = vi.mocked(useToolOperation);
const getToolConfig = (): ToolOperationConfig<RemovePasswordParameters> => mockUseToolOperation.mock.calls[0][0];
const mockToolOperationReturn: ToolOperationHook<unknown> = {
files: [],
thumbnails: [],
downloadUrl: null,
downloadFilename: '',
isLoading: false,
errorMessage: null,
status: '',
isGeneratingThumbnails: false,
progress: null,
executeOperation: vi.fn(),
resetResults: vi.fn(),
clearError: vi.fn(),
cancelOperation: vi.fn(),
};
beforeEach(() => {
vi.clearAllMocks();
mockUseToolOperation.mockReturnValue(mockToolOperationReturn);
});
test.each([
{
description: 'with valid password',
password: 'test-password'
},
{
description: 'with complex password',
password: 'C0mpl3x@P@ssw0rd!'
},
{
description: 'with single character password',
password: 'a'
}
])('should create form data correctly $description', ({ password }) => {
renderHook(() => useRemovePasswordOperation());
const callArgs = getToolConfig();
const buildFormData = callArgs.buildFormData;
const testParameters: RemovePasswordParameters = {
password
};
const testFile = new File(['test content'], 'test.pdf', { type: 'application/pdf' });
const formData = buildFormData(testParameters, testFile as any);
// Verify the form data contains the file
expect(formData.get('fileInput')).toBe(testFile);
// Verify password parameter
expect(formData.get('password')).toBe(password);
});
test('should use correct translation for error messages', () => {
renderHook(() => useRemovePasswordOperation());
expect(mockT).toHaveBeenCalledWith(
'removePassword.error.failed',
'An error occurred while removing the password from the PDF.'
);
});
test.each([
{ property: 'multiFileEndpoint' as const, expectedValue: false },
{ property: 'endpoint' as const, expectedValue: '/api/v1/security/remove-password' },
{ property: 'filePrefix' as const, expectedValue: 'translated-removePassword.filenamePrefix_' },
{ property: 'operationType' as const, expectedValue: 'removePassword' }
])('should configure $property correctly', ({ property, expectedValue }) => {
renderHook(() => useRemovePasswordOperation());
const callArgs = getToolConfig();
expect(callArgs[property]).toBe(expectedValue);
});
});
@@ -0,0 +1,24 @@
import { useTranslation } from 'react-i18next';
import { useToolOperation } from '../shared/useToolOperation';
import { createStandardErrorHandler } from '../../../utils/toolErrorHandler';
import { RemovePasswordParameters } from './useRemovePasswordParameters';
export const useRemovePasswordOperation = () => {
const { t } = useTranslation();
const buildFormData = (parameters: RemovePasswordParameters, file: File): FormData => {
const formData = new FormData();
formData.append("fileInput", file);
formData.append("password", parameters.password);
return formData;
};
return useToolOperation<RemovePasswordParameters>({
operationType: 'removePassword',
endpoint: '/api/v1/security/remove-password',
buildFormData,
filePrefix: t('removePassword.filenamePrefix', 'decrypted') + '_',
multiFileEndpoint: false,
getErrorMessage: createStandardErrorHandler(t('removePassword.error.failed', 'An error occurred while removing the password from the PDF.'))
});
};
@@ -0,0 +1,81 @@
import { describe, expect, test } from 'vitest';
import { renderHook, act } from '@testing-library/react';
import { useRemovePasswordParameters, defaultParameters } from './useRemovePasswordParameters';
describe('useRemovePasswordParameters', () => {
test('should initialize with default parameters', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should update password parameter', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
act(() => {
result.current.updateParameter('password', 'test-password');
});
expect(result.current.parameters.password).toBe('test-password');
});
test('should reset parameters to defaults', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
// First, change the password
act(() => {
result.current.updateParameter('password', 'test-password');
});
expect(result.current.parameters.password).toBe('test-password');
// Then reset
act(() => {
result.current.resetParameters();
});
expect(result.current.parameters).toStrictEqual(defaultParameters);
});
test('should return correct endpoint name', () => {
const { result } = renderHook(() => useRemovePasswordParameters());
expect(result.current.getEndpointName()).toBe('remove-password');
});
test.each([
{
description: 'with valid password',
password: 'valid-password',
expectedValid: true
},
{
description: 'with empty password',
password: '',
expectedValid: false
},
{
description: 'with whitespace only password',
password: ' \t ',
expectedValid: true
},
{
description: 'with password containing special characters',
password: 'p@ssw0rd!',
expectedValid: true
},
{
description: 'with single character password',
password: 'a',
expectedValid: true
}
])('should validate parameters correctly $description', ({ password, expectedValid }) => {
const { result } = renderHook(() => useRemovePasswordParameters());
act(() => {
result.current.updateParameter('password', password);
});
expect(result.current.validateParameters()).toBe(expectedValid);
});
});
@@ -0,0 +1,49 @@
import { useState } from 'react';
export interface RemovePasswordParameters {
password: string;
}
export interface RemovePasswordParametersHook {
parameters: RemovePasswordParameters;
updateParameter: <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => void;
resetParameters: () => void;
validateParameters: () => boolean;
getEndpointName: () => string;
}
export const defaultParameters: RemovePasswordParameters = {
password: '',
};
export const useRemovePasswordParameters = (): RemovePasswordParametersHook => {
const [parameters, setParameters] = useState<RemovePasswordParameters>(defaultParameters);
const updateParameter = <K extends keyof RemovePasswordParameters>(parameter: K, value: RemovePasswordParameters[K]) => {
setParameters(prev => ({
...prev,
[parameter]: value,
})
);
};
const resetParameters = () => {
setParameters(defaultParameters);
};
const validateParameters = () => {
return parameters.password !== '';
};
const getEndpointName = () => {
return 'remove-password';
};
return {
parameters,
updateParameter,
resetParameters,
validateParameters,
getEndpointName,
};
};