feat(tests): implement and update tests for multiple OCR languages

This commit is contained in:
perf3ct 2025-06-29 23:03:37 +00:00
parent 6b6890d529
commit 197afc19f4
8 changed files with 2184 additions and 0 deletions

View File

@ -0,0 +1,276 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import OcrLanguageSelector from '../OcrLanguageSelector';
import { ocrService } from '../../../services/api';
// Mock the API service
vi.mock('../../../services/api', () => ({
ocrService: {
getAvailableLanguages: vi.fn(),
},
}));
const mockOcrService = vi.mocked(ocrService);
const theme = createTheme();
const renderWithTheme = (component: React.ReactElement) => {
return render(
<ThemeProvider theme={theme}>
{component}
</ThemeProvider>
);
};
describe('OcrLanguageSelector', () => {
const defaultProps = {
value: 'eng',
onChange: vi.fn(),
label: 'OCR Language',
};
const mockLanguagesResponse = {
data: {
languages: [
{ code: 'eng', name: 'English' },
{ code: 'spa', name: 'Spanish' },
{ code: 'fra', name: 'French' },
{ code: 'deu', name: 'German' },
],
current_user_language: 'eng',
},
};
beforeEach(() => {
vi.clearAllMocks();
mockOcrService.getAvailableLanguages.mockResolvedValue(mockLanguagesResponse);
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders with default props', async () => {
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
expect(screen.getByLabelText('OCR Language')).toBeInTheDocument();
// Wait for languages to load
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
});
it('displays loading state initially', () => {
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
expect(screen.getByTestId('loading-languages')).toBeInTheDocument();
});
it('loads and displays available languages', async () => {
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
// Open the select dropdown
fireEvent.mouseDown(screen.getByRole('combobox'));
await waitFor(() => {
expect(screen.getByText('English')).toBeInTheDocument();
expect(screen.getByText('Spanish')).toBeInTheDocument();
expect(screen.getByText('French')).toBeInTheDocument();
expect(screen.getByText('German')).toBeInTheDocument();
});
});
it('shows current language indicator when enabled', async () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
showCurrentIndicator={true}
/>
);
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
// Open the select dropdown
fireEvent.mouseDown(screen.getByRole('combobox'));
await waitFor(() => {
expect(screen.getByText('(Current)')).toBeInTheDocument();
});
});
it('calls onChange when language is selected', async () => {
const mockOnChange = vi.fn();
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
onChange={mockOnChange}
/>
);
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
// Open the select dropdown
fireEvent.mouseDown(screen.getByRole('combobox'));
// Select Spanish
fireEvent.click(screen.getByText('Spanish'));
expect(mockOnChange).toHaveBeenCalledWith('spa');
});
it('displays error state when API call fails', async () => {
const mockError = new Error('Failed to fetch languages');
mockOcrService.getAvailableLanguages.mockRejectedValue(mockError);
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('Failed to load languages')).toBeInTheDocument();
});
});
it('retries loading languages when retry button is clicked', async () => {
const mockError = new Error('Failed to fetch languages');
mockOcrService.getAvailableLanguages.mockRejectedValueOnce(mockError);
mockOcrService.getAvailableLanguages.mockResolvedValueOnce(mockLanguagesResponse);
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
// Wait for error state
await waitFor(() => {
expect(screen.getByText('Failed to load languages')).toBeInTheDocument();
});
// Click retry button
fireEvent.click(screen.getByText('Retry'));
// Should call API again
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(2);
});
});
it('renders with custom label', () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
label="Custom Language Label"
/>
);
expect(screen.getByLabelText('Custom Language Label')).toBeInTheDocument();
});
it('renders with helper text', () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
helperText="Choose your preferred language"
/>
);
expect(screen.getByText('Choose your preferred language')).toBeInTheDocument();
});
it('respects size prop', () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
size="small"
/>
);
const select = screen.getByRole('combobox');
expect(select).toHaveClass('MuiInputBase-sizeSmall');
});
it('respects disabled prop', () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
disabled={true}
/>
);
const select = screen.getByRole('combobox');
expect(select).toBeDisabled();
});
it('handles empty language list gracefully', async () => {
mockOcrService.getAvailableLanguages.mockResolvedValue({
data: {
languages: [],
current_user_language: null,
},
});
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
// Open the select dropdown
fireEvent.mouseDown(screen.getByRole('combobox'));
await waitFor(() => {
expect(screen.getByText('No languages available')).toBeInTheDocument();
});
});
it('displays selected language correctly', async () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
value="spa"
/>
);
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
// The selected value should be displayed
expect(screen.getByDisplayValue('spa')).toBeInTheDocument();
});
it('handles network errors gracefully', async () => {
const networkError = new Error('Network Error');
networkError.name = 'NetworkError';
mockOcrService.getAvailableLanguages.mockRejectedValue(networkError);
renderWithTheme(<OcrLanguageSelector {...defaultProps} />);
await waitFor(() => {
expect(screen.getByText('Failed to load languages')).toBeInTheDocument();
expect(screen.getByText('Check your internet connection')).toBeInTheDocument();
});
});
it('clears selection when value is empty string', async () => {
renderWithTheme(
<OcrLanguageSelector
{...defaultProps}
value=""
/>
);
await waitFor(() => {
expect(mockOcrService.getAvailableLanguages).toHaveBeenCalledTimes(1);
});
const select = screen.getByRole('combobox');
expect(select).toHaveValue('');
});
});

View File

@ -0,0 +1,374 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import OcrRetryDialog from '../OcrRetryDialog';
import { ocrService } from '../../../services/api';
// Mock the API service
vi.mock('../../../services/api', () => ({
ocrService: {
getAvailableLanguages: vi.fn(),
retryWithLanguage: vi.fn(),
},
}));
// Mock the OcrLanguageSelector component
vi.mock('../../OcrLanguageSelector', () => ({
default: ({ value, onChange, ...props }: any) => (
<div data-testid="ocr-language-selector">
<select
value={value}
onChange={(e) => onChange(e.target.value)}
data-testid="language-select"
{...props}
>
<option value="">Select language</option>
<option value="eng">English</option>
<option value="spa">Spanish</option>
<option value="fra">French</option>
</select>
</div>
),
}));
const mockOcrService = vi.mocked(ocrService);
const theme = createTheme();
const renderWithTheme = (component: React.ReactElement) => {
return render(
<ThemeProvider theme={theme}>
{component}
</ThemeProvider>
);
};
describe('OcrRetryDialog', () => {
const mockDocument = {
id: 'doc-123',
filename: 'test-document.pdf',
original_filename: 'test-document.pdf',
failure_category: 'Language Detection Failed',
ocr_error: 'Unable to detect text language',
retry_count: 2,
};
const defaultProps = {
open: true,
onClose: vi.fn(),
document: mockDocument,
onRetrySuccess: vi.fn(),
onRetryError: vi.fn(),
};
const mockRetryResponse = {
data: {
success: true,
message: 'OCR retry queued successfully',
estimated_wait_minutes: 5,
},
};
beforeEach(() => {
vi.clearAllMocks();
mockOcrService.retryWithLanguage.mockResolvedValue(mockRetryResponse);
});
afterEach(() => {
vi.clearAllMocks();
});
it('renders dialog when open is true', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} />);
expect(screen.getByText('Retry OCR Processing')).toBeInTheDocument();
expect(screen.getByText('Document: test-document.pdf')).toBeInTheDocument();
expect(screen.getByText('Previous attempts: 2')).toBeInTheDocument();
});
it('does not render dialog when open is false', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} open={false} />);
expect(screen.queryByText('Retry OCR Processing')).not.toBeInTheDocument();
});
it('does not render when document is null', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} document={null} />);
expect(screen.queryByText('Retry OCR Processing')).not.toBeInTheDocument();
});
it('displays document information correctly', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} />);
expect(screen.getByText('Document: test-document.pdf')).toBeInTheDocument();
expect(screen.getByText('Previous attempts: 2')).toBeInTheDocument();
expect(screen.getByText('Previous failure: Language Detection Failed')).toBeInTheDocument();
expect(screen.getByText('Unable to detect text language')).toBeInTheDocument();
});
it('renders language selector', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} />);
expect(screen.getByTestId('ocr-language-selector')).toBeInTheDocument();
expect(screen.getByText('OCR Language Selection')).toBeInTheDocument();
});
it('handles language selection', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} />);
const languageSelect = screen.getByTestId('language-select');
fireEvent.change(languageSelect, { target: { value: 'spa' } });
expect(languageSelect).toHaveValue('spa');
});
it('calls onRetrySuccess when retry succeeds', async () => {
const mockOnRetrySuccess = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onRetrySuccess={mockOnRetrySuccess}
/>
);
// Select a language
const languageSelect = screen.getByTestId('language-select');
fireEvent.change(languageSelect, { target: { value: 'spa' } });
// Click retry button
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOcrService.retryWithLanguage).toHaveBeenCalledWith('doc-123', 'spa');
expect(mockOnRetrySuccess).toHaveBeenCalledWith(
'OCR retry queued for "test-document.pdf" with language "Spanish". Estimated wait time: 5 minutes.'
);
});
});
it('calls onRetrySuccess without language info when no language selected', async () => {
const mockOnRetrySuccess = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onRetrySuccess={mockOnRetrySuccess}
/>
);
// Click retry button without selecting language
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOcrService.retryWithLanguage).toHaveBeenCalledWith('doc-123', undefined);
expect(mockOnRetrySuccess).toHaveBeenCalledWith(
'OCR retry queued for "test-document.pdf". Estimated wait time: 5 minutes.'
);
});
});
it('handles retry failure', async () => {
const mockError = new Error('Retry failed');
mockOcrService.retryWithLanguage.mockRejectedValue(mockError);
const mockOnRetryError = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onRetryError={mockOnRetryError}
/>
);
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOnRetryError).toHaveBeenCalledWith('Failed to retry OCR processing');
});
});
it('handles API error response', async () => {
const mockErrorResponse = {
response: {
data: {
message: 'Document not found',
},
},
};
mockOcrService.retryWithLanguage.mockRejectedValue(mockErrorResponse);
const mockOnRetryError = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onRetryError={mockOnRetryError}
/>
);
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOnRetryError).toHaveBeenCalledWith('Document not found');
});
});
it('handles unsuccessful retry response', async () => {
mockOcrService.retryWithLanguage.mockResolvedValue({
data: {
success: false,
message: 'Queue is full',
},
});
const mockOnRetryError = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onRetryError={mockOnRetryError}
/>
);
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOnRetryError).toHaveBeenCalledWith('Queue is full');
});
});
it('shows loading state during retry', async () => {
// Make the API call hang
mockOcrService.retryWithLanguage.mockImplementation(() => new Promise(() => {}));
renderWithTheme(<OcrRetryDialog {...defaultProps} />);
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(screen.getByText('Retrying...')).toBeInTheDocument();
});
// Buttons should be disabled during retry
expect(screen.getByText('Cancel')).toBeDisabled();
expect(screen.getByText('Retrying...')).toBeDisabled();
});
it('prevents closing dialog during retry', async () => {
// Make the API call hang
mockOcrService.retryWithLanguage.mockImplementation(() => new Promise(() => {}));
const mockOnClose = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onClose={mockOnClose}
/>
);
fireEvent.click(screen.getByText('Retry OCR'));
// Try to close via cancel button
fireEvent.click(screen.getByText('Cancel'));
// Should not call onClose during retry
expect(mockOnClose).not.toHaveBeenCalled();
});
it('calls onClose when cancel is clicked', () => {
const mockOnClose = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onClose={mockOnClose}
/>
);
fireEvent.click(screen.getByText('Cancel'));
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
it('clears selected language when dialog closes', () => {
const mockOnClose = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onClose={mockOnClose}
/>
);
// Select a language
const languageSelect = screen.getByTestId('language-select');
fireEvent.change(languageSelect, { target: { value: 'spa' } });
// Close dialog
fireEvent.click(screen.getByText('Cancel'));
expect(mockOnClose).toHaveBeenCalled();
});
it('closes dialog after successful retry', async () => {
const mockOnClose = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onClose={mockOnClose}
/>
);
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOnClose).toHaveBeenCalledTimes(1);
});
});
it('displays informational message about retry process', () => {
renderWithTheme(<OcrRetryDialog {...defaultProps} />);
expect(screen.getByText(/The retry will use enhanced OCR processing/)).toBeInTheDocument();
});
it('handles document without failure category', () => {
const documentWithoutFailure = {
...mockDocument,
failure_category: '',
ocr_error: '',
};
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
document={documentWithoutFailure}
/>
);
expect(screen.getByText('Document: test-document.pdf')).toBeInTheDocument();
expect(screen.queryByText('Previous failure:')).not.toBeInTheDocument();
});
it('handles missing estimated wait time in response', async () => {
mockOcrService.retryWithLanguage.mockResolvedValue({
data: {
success: true,
message: 'OCR retry queued successfully',
// No estimated_wait_minutes
},
});
const mockOnRetrySuccess = vi.fn();
renderWithTheme(
<OcrRetryDialog
{...defaultProps}
onRetrySuccess={mockOnRetrySuccess}
/>
);
fireEvent.click(screen.getByText('Retry OCR'));
await waitFor(() => {
expect(mockOnRetrySuccess).toHaveBeenCalledWith(
'OCR retry queued for "test-document.pdf". Estimated wait time: Unknown minutes.'
);
});
});
});

View File

@ -0,0 +1,368 @@
import React from 'react';
import { render, screen, fireEvent, waitFor } from '@testing-library/react';
import { vi, describe, it, expect, beforeEach, afterEach } from 'vitest';
import { BrowserRouter } from 'react-router-dom';
import { ThemeProvider, createTheme } from '@mui/material/styles';
import SettingsPage from '../SettingsPage';
import { AuthContext } from '../../contexts/AuthContext';
import api, { ocrService } from '../../services/api';
// Mock the API
vi.mock('../../services/api', () => ({
default: {
get: vi.fn(),
post: vi.fn(),
put: vi.fn(),
},
ocrService: {
getAvailableLanguages: vi.fn(),
},
queueService: {
getQueueStats: vi.fn(),
},
}));
const mockedApi = vi.mocked(api);
const mockedOcrService = vi.mocked(ocrService);
const theme = createTheme();
const mockAuthContext = {
user: {
id: 'user-123',
username: 'testuser',
email: 'test@example.com',
created_at: '2023-01-01T00:00:00Z',
},
login: vi.fn(),
logout: vi.fn(),
loading: false,
};
const renderWithProviders = (component: React.ReactElement) => {
return render(
<BrowserRouter>
<ThemeProvider theme={theme}>
<AuthContext.Provider value={mockAuthContext}>
{component}
</AuthContext.Provider>
</ThemeProvider>
</BrowserRouter>
);
};
describe('Settings Page - OCR Language Integration', () => {
const mockSettingsResponse = {
data: {
ocrLanguage: 'eng',
concurrentOcrJobs: 2,
ocrTimeoutSeconds: 300,
maxFileSizeMb: 50,
allowedFileTypes: ['pdf', 'png', 'jpg'],
autoRotateImages: true,
enableImagePreprocessing: true,
searchResultsPerPage: 20,
searchSnippetLength: 200,
fuzzySearchThreshold: 0.7,
retentionDays: null,
enableAutoCleanup: false,
enableCompression: true,
memoryLimitMb: 1024,
cpuPriority: 'normal',
enableBackgroundOcr: true,
ocrPageSegmentationMode: 3,
ocrEngineMode: 3,
ocrMinConfidence: 30,
ocrDpi: 300,
ocrEnhanceContrast: true,
ocrRemoveNoise: true,
ocrDetectOrientation: true,
ocrWhitelistChars: '',
ocrBlacklistChars: '',
ocrBrightnessBoost: 0,
ocrContrastMultiplier: 1.0,
ocrNoiseReductionLevel: 1,
ocrSharpeningStrength: 0,
ocrMorphologicalOperations: false,
ocrAdaptiveThresholdWindowSize: 15,
ocrHistogramEqualization: false,
ocrUpscaleFactor: 1.0,
ocrMaxImageWidth: 4000,
ocrMaxImageHeight: 4000,
saveProcessedImages: false,
ocrQualityThresholdBrightness: 50,
ocrQualityThresholdContrast: 20,
ocrQualityThresholdNoise: 80,
ocrQualityThresholdSharpness: 30,
ocrSkipEnhancement: false,
},
};
const mockLanguagesResponse = {
data: {
languages: [
{ code: 'eng', name: 'English' },
{ code: 'spa', name: 'Spanish' },
{ code: 'fra', name: 'French' },
{ code: 'deu', name: 'German' },
{ code: 'ita', name: 'Italian' },
],
current_user_language: 'eng',
},
};
const mockQueueStatsResponse = {
data: {
total_jobs: 0,
pending_jobs: 0,
processing_jobs: 0,
completed_jobs: 0,
failed_jobs: 0,
},
};
beforeEach(() => {
vi.clearAllMocks();
mockedApi.get.mockImplementation((url) => {
if (url === '/settings') return Promise.resolve(mockSettingsResponse);
if (url === '/labels?include_counts=true') return Promise.resolve({ data: [] });
return Promise.reject(new Error(`Unexpected GET request to ${url}`));
});
mockedOcrService.getAvailableLanguages.mockResolvedValue(mockLanguagesResponse);
vi.mocked(require('../../services/api').queueService.getQueueStats).mockResolvedValue(mockQueueStatsResponse);
});
afterEach(() => {
vi.clearAllMocks();
});
it('loads and displays current OCR language in settings', async () => {
renderWithProviders(<SettingsPage />);
await waitFor(() => {
expect(mockedApi.get).toHaveBeenCalledWith('/settings');
expect(mockedOcrService.getAvailableLanguages).toHaveBeenCalled();
});
// Should display the OCR language selector
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
it('successfully changes OCR language and saves settings', async () => {
const mockUpdateResponse = { data: { success: true } };
mockedApi.put.mockResolvedValueOnce(mockUpdateResponse);
renderWithProviders(<SettingsPage />);
// Wait for page to load
await waitFor(() => {
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
// Find and open the language selector
const languageSelector = screen.getByLabelText('OCR Language');
fireEvent.mouseDown(languageSelector);
// Wait for dropdown options to appear
await waitFor(() => {
expect(screen.getByText('Spanish')).toBeInTheDocument();
});
// Select Spanish
fireEvent.click(screen.getByText('Spanish'));
// Find and click the save button
const saveButton = screen.getByText('Save Changes');
fireEvent.click(saveButton);
// Verify the API call was made with updated settings
await waitFor(() => {
expect(mockedApi.put).toHaveBeenCalledWith('/settings', {
...mockSettingsResponse.data,
ocrLanguage: 'spa',
});
});
// Should show success message
await waitFor(() => {
expect(screen.getByText('Settings saved successfully')).toBeInTheDocument();
});
});
it('handles OCR language loading errors gracefully', async () => {
mockedOcrService.getAvailableLanguages.mockRejectedValueOnce(new Error('Failed to load languages'));
renderWithProviders(<SettingsPage />);
await waitFor(() => {
expect(mockedOcrService.getAvailableLanguages).toHaveBeenCalled();
});
// Should still render the page but with error state in language selector
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
it('handles settings save errors appropriately', async () => {
const mockError = new Error('Failed to save settings');
mockedApi.put.mockRejectedValueOnce(mockError);
renderWithProviders(<SettingsPage />);
// Wait for page to load
await waitFor(() => {
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
// Change a setting
const languageSelector = screen.getByLabelText('OCR Language');
fireEvent.mouseDown(languageSelector);
await waitFor(() => {
expect(screen.getByText('French')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('French'));
// Try to save
const saveButton = screen.getByText('Save Changes');
fireEvent.click(saveButton);
// Should show error message
await waitFor(() => {
expect(screen.getByText(/Failed to save settings/)).toBeInTheDocument();
});
});
it('preserves other settings when changing OCR language', async () => {
const mockUpdateResponse = { data: { success: true } };
mockedApi.put.mockResolvedValueOnce(mockUpdateResponse);
renderWithProviders(<SettingsPage />);
// Wait for page to load
await waitFor(() => {
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
// Change OCR language
const languageSelector = screen.getByLabelText('OCR Language');
fireEvent.mouseDown(languageSelector);
await waitFor(() => {
expect(screen.getByText('German')).toBeInTheDocument();
});
fireEvent.click(screen.getByText('German'));
// Save settings
const saveButton = screen.getByText('Save Changes');
fireEvent.click(saveButton);
// Verify all original settings are preserved except OCR language
await waitFor(() => {
expect(mockedApi.put).toHaveBeenCalledWith('/settings', {
...mockSettingsResponse.data,
ocrLanguage: 'deu',
});
});
});
it('shows loading state while fetching languages', async () => {
// Make the language fetch hang
mockedOcrService.getAvailableLanguages.mockImplementation(() => new Promise(() => {}));
renderWithProviders(<SettingsPage />);
await waitFor(() => {
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
// Should show loading indicator in the language selector
expect(screen.getByTestId('loading-languages')).toBeInTheDocument();
});
it('handles empty language list', async () => {
mockedOcrService.getAvailableLanguages.mockResolvedValueOnce({
data: {
languages: [],
current_user_language: null,
},
});
renderWithProviders(<SettingsPage />);
await waitFor(() => {
expect(mockedOcrService.getAvailableLanguages).toHaveBeenCalled();
});
// Should still render the language selector
expect(screen.getByText('OCR Language')).toBeInTheDocument();
// Open the dropdown
const languageSelector = screen.getByLabelText('OCR Language');
fireEvent.mouseDown(languageSelector);
// Should show "No languages available"
await waitFor(() => {
expect(screen.getByText('No languages available')).toBeInTheDocument();
});
});
it('indicates current user language in the dropdown', async () => {
renderWithProviders(<SettingsPage />);
await waitFor(() => {
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
// Open the language selector
const languageSelector = screen.getByLabelText('OCR Language');
fireEvent.mouseDown(languageSelector);
// Should show current language indicator
await waitFor(() => {
expect(screen.getByText('(Current)')).toBeInTheDocument();
});
});
it('updates language selector when settings are reloaded', async () => {
const { rerender } = renderWithProviders(<SettingsPage />);
// Initial load
await waitFor(() => {
expect(screen.getByText('OCR Language')).toBeInTheDocument();
});
// Update mock to return different language
const updatedSettingsResponse = {
...mockSettingsResponse,
data: {
...mockSettingsResponse.data,
ocrLanguage: 'spa',
},
};
mockedApi.get.mockImplementation((url) => {
if (url === '/settings') return Promise.resolve(updatedSettingsResponse);
if (url === '/labels?include_counts=true') return Promise.resolve({ data: [] });
return Promise.reject(new Error(`Unexpected GET request to ${url}`));
});
// Rerender component
rerender(
<BrowserRouter>
<ThemeProvider theme={theme}>
<AuthContext.Provider value={mockAuthContext}>
<SettingsPage />
</AuthContext.Provider>
</ThemeProvider>
</BrowserRouter>
);
// Should reflect the updated language
await waitFor(() => {
const languageSelector = screen.getByLabelText('OCR Language');
expect(languageSelector).toHaveValue('spa');
});
});
});

View File

@ -0,0 +1,335 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import axios from 'axios';
import { ocrService } from '../api';
// Mock axios
vi.mock('axios');
const mockedAxios = vi.mocked(axios);
describe('OCR API Service', () => {
beforeEach(() => {
vi.clearAllMocks();
});
afterEach(() => {
vi.clearAllMocks();
});
describe('getAvailableLanguages', () => {
it('should fetch available languages successfully', async () => {
const mockResponse = {
data: {
languages: [
{ code: 'eng', name: 'English' },
{ code: 'spa', name: 'Spanish' },
{ code: 'fra', name: 'French' },
],
current_user_language: 'eng',
},
status: 200,
};
mockedAxios.get.mockResolvedValueOnce(mockResponse);
const result = await ocrService.getAvailableLanguages();
expect(mockedAxios.get).toHaveBeenCalledWith('/ocr/languages');
expect(result).toEqual(mockResponse);
});
it('should handle network errors', async () => {
const networkError = new Error('Network Error');
mockedAxios.get.mockRejectedValueOnce(networkError);
await expect(ocrService.getAvailableLanguages()).rejects.toThrow('Network Error');
expect(mockedAxios.get).toHaveBeenCalledWith('/ocr/languages');
});
it('should handle empty language list', async () => {
const mockResponse = {
data: {
languages: [],
current_user_language: null,
},
status: 200,
};
mockedAxios.get.mockResolvedValueOnce(mockResponse);
const result = await ocrService.getAvailableLanguages();
expect(result.data.languages).toEqual([]);
expect(result.data.current_user_language).toBeNull();
});
});
describe('getHealthStatus', () => {
it('should fetch OCR health status successfully', async () => {
const mockResponse = {
data: {
status: 'healthy',
tesseract_version: '5.3.0',
available_languages: ['eng', 'spa', 'fra'],
},
status: 200,
};
mockedAxios.get.mockResolvedValueOnce(mockResponse);
const result = await ocrService.getHealthStatus();
expect(mockedAxios.get).toHaveBeenCalledWith('/ocr/health');
expect(result).toEqual(mockResponse);
});
it('should handle unhealthy OCR service', async () => {
const mockResponse = {
data: {
status: 'unhealthy',
error: 'Tesseract not found',
},
status: 503,
};
mockedAxios.get.mockResolvedValueOnce(mockResponse);
const result = await ocrService.getHealthStatus();
expect(result.data.status).toBe('unhealthy');
expect(result.data.error).toBe('Tesseract not found');
});
});
describe('retryWithLanguage', () => {
const documentId = 'doc-123';
it('should retry OCR without language parameter', async () => {
const mockResponse = {
data: {
success: true,
message: 'OCR retry queued successfully',
queue_id: 'queue-456',
estimated_wait_minutes: 5,
},
status: 200,
};
mockedAxios.post.mockResolvedValueOnce(mockResponse);
const result = await ocrService.retryWithLanguage(documentId);
expect(mockedAxios.post).toHaveBeenCalledWith(
`/documents/${documentId}/retry-ocr`,
{}
);
expect(result).toEqual(mockResponse);
});
it('should retry OCR with language parameter', async () => {
const language = 'spa';
const mockResponse = {
data: {
success: true,
message: 'OCR retry queued successfully',
queue_id: 'queue-456',
estimated_wait_minutes: 3,
},
status: 200,
};
mockedAxios.post.mockResolvedValueOnce(mockResponse);
const result = await ocrService.retryWithLanguage(documentId, language);
expect(mockedAxios.post).toHaveBeenCalledWith(
`/documents/${documentId}/retry-ocr`,
{ language: 'spa' }
);
expect(result).toEqual(mockResponse);
});
it('should handle retry failure', async () => {
const errorResponse = {
response: {
data: {
success: false,
message: 'Document not found',
},
status: 404,
},
};
mockedAxios.post.mockRejectedValueOnce(errorResponse);
await expect(ocrService.retryWithLanguage(documentId)).rejects.toEqual(errorResponse);
});
it('should handle queue full error', async () => {
const errorResponse = {
response: {
data: {
success: false,
message: 'OCR queue is currently full. Please try again later.',
},
status: 429,
},
};
mockedAxios.post.mockRejectedValueOnce(errorResponse);
await expect(ocrService.retryWithLanguage(documentId, 'eng')).rejects.toEqual(errorResponse);
});
it('should handle invalid language error', async () => {
const errorResponse = {
response: {
data: {
success: false,
message: 'Language "invalid" is not supported',
},
status: 400,
},
};
mockedAxios.post.mockRejectedValueOnce(errorResponse);
await expect(ocrService.retryWithLanguage(documentId, 'invalid')).rejects.toEqual(errorResponse);
});
it('should handle network timeout', async () => {
const timeoutError = new Error('timeout of 10000ms exceeded');
timeoutError.name = 'TimeoutError';
mockedAxios.post.mockRejectedValueOnce(timeoutError);
await expect(ocrService.retryWithLanguage(documentId)).rejects.toThrow('timeout of 10000ms exceeded');
});
it('should handle empty string language as undefined', async () => {
const mockResponse = {
data: {
success: true,
message: 'OCR retry queued successfully',
},
status: 200,
};
mockedAxios.post.mockResolvedValueOnce(mockResponse);
await ocrService.retryWithLanguage(documentId, '');
expect(mockedAxios.post).toHaveBeenCalledWith(
`/documents/${documentId}/retry-ocr`,
{}
);
});
it('should preserve language whitespace and special characters', async () => {
const language = 'chi_sim'; // Chinese Simplified
const mockResponse = {
data: {
success: true,
message: 'OCR retry queued successfully',
},
status: 200,
};
mockedAxios.post.mockResolvedValueOnce(mockResponse);
await ocrService.retryWithLanguage(documentId, language);
expect(mockedAxios.post).toHaveBeenCalledWith(
`/documents/${documentId}/retry-ocr`,
{ language: 'chi_sim' }
);
});
});
describe('Error Handling', () => {
it('should handle 401 unauthorized errors', async () => {
const unauthorizedError = {
response: {
status: 401,
data: {
message: 'Unauthorized',
},
},
};
mockedAxios.get.mockRejectedValueOnce(unauthorizedError);
await expect(ocrService.getAvailableLanguages()).rejects.toEqual(unauthorizedError);
});
it('should handle 403 forbidden errors', async () => {
const forbiddenError = {
response: {
status: 403,
data: {
message: 'Insufficient permissions',
},
},
};
mockedAxios.get.mockRejectedValueOnce(forbiddenError);
await expect(ocrService.getHealthStatus()).rejects.toEqual(forbiddenError);
});
it('should handle 500 internal server errors', async () => {
const serverError = {
response: {
status: 500,
data: {
message: 'Internal server error',
},
},
};
mockedAxios.post.mockRejectedValueOnce(serverError);
await expect(ocrService.retryWithLanguage('doc-123')).rejects.toEqual(serverError);
});
it('should handle malformed response data', async () => {
const malformedResponse = {
data: null,
status: 200,
};
mockedAxios.get.mockResolvedValueOnce(malformedResponse);
const result = await ocrService.getAvailableLanguages();
expect(result.data).toBeNull();
});
});
describe('Request Configuration', () => {
it('should use correct base URL', async () => {
const mockResponse = { data: {}, status: 200 };
mockedAxios.get.mockResolvedValueOnce(mockResponse);
await ocrService.getAvailableLanguages();
expect(mockedAxios.get).toHaveBeenCalledWith('/ocr/languages');
});
it('should handle concurrent requests', async () => {
const mockResponse = { data: {}, status: 200 };
mockedAxios.get.mockResolvedValue(mockResponse);
mockedAxios.post.mockResolvedValue(mockResponse);
const requests = [
ocrService.getAvailableLanguages(),
ocrService.getHealthStatus(),
ocrService.retryWithLanguage('doc-1', 'eng'),
ocrService.retryWithLanguage('doc-2', 'spa'),
];
await Promise.all(requests);
expect(mockedAxios.get).toHaveBeenCalledTimes(2);
expect(mockedAxios.post).toHaveBeenCalledTimes(2);
});
});
});

View File

@ -8,6 +8,9 @@ mod tests {
use tempfile::TempDir;
use std::fs;
// Include language validation tests
mod language_validation_tests;
#[test]
fn test_ocr_error_types() {
// Test error creation and properties

View File

@ -0,0 +1,298 @@
#[cfg(test)]
mod language_validation_tests {
use super::super::health::{OcrHealthChecker, OcrError};
use std::path::Path;
use tempfile::TempDir;
use std::fs;
fn create_test_health_checker() -> (OcrHealthChecker, TempDir) {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let tessdata_path = temp_dir.path().join("tessdata");
fs::create_dir_all(&tessdata_path).expect("Failed to create tessdata directory");
// Create mock language files
let language_files = vec![
"eng.traineddata",
"spa.traineddata",
"fra.traineddata",
"deu.traineddata",
"chi_sim.traineddata",
];
for file in language_files {
fs::write(tessdata_path.join(file), "mock data")
.expect("Failed to create mock language file");
}
let health_checker = OcrHealthChecker::new(tessdata_path);
(health_checker, temp_dir)
}
#[test]
fn test_get_available_languages_success() {
let (health_checker, _temp_dir) = create_test_health_checker();
let result = health_checker.get_available_languages();
assert!(result.is_ok());
let languages = result.unwrap();
assert_eq!(languages.len(), 5);
assert!(languages.contains(&"eng".to_string()));
assert!(languages.contains(&"spa".to_string()));
assert!(languages.contains(&"fra".to_string()));
assert!(languages.contains(&"deu".to_string()));
assert!(languages.contains(&"chi_sim".to_string()));
}
#[test]
fn test_get_available_languages_empty_directory() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let tessdata_path = temp_dir.path().join("tessdata");
fs::create_dir_all(&tessdata_path).expect("Failed to create tessdata directory");
let health_checker = OcrHealthChecker::new(tessdata_path);
let result = health_checker.get_available_languages();
assert!(result.is_ok());
let languages = result.unwrap();
assert!(languages.is_empty());
}
#[test]
fn test_get_available_languages_nonexistent_directory() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let nonexistent_path = temp_dir.path().join("nonexistent");
let health_checker = OcrHealthChecker::new(nonexistent_path);
let result = health_checker.get_available_languages();
assert!(result.is_err());
match result.unwrap_err() {
OcrError::TessdataPathNotFound { .. } => {},
_ => panic!("Expected TessdataPathNotFound error"),
}
}
#[test]
fn test_validate_language_success() {
let (health_checker, _temp_dir) = create_test_health_checker();
// Test valid languages
assert!(health_checker.validate_language("eng").is_ok());
assert!(health_checker.validate_language("spa").is_ok());
assert!(health_checker.validate_language("fra").is_ok());
assert!(health_checker.validate_language("deu").is_ok());
assert!(health_checker.validate_language("chi_sim").is_ok());
}
#[test]
fn test_validate_language_invalid() {
let (health_checker, _temp_dir) = create_test_health_checker();
// Test invalid languages
let result = health_checker.validate_language("invalid");
assert!(result.is_err());
match result.unwrap_err() {
OcrError::LanguageDataNotFound { lang } => {
assert_eq!(lang, "invalid");
},
_ => panic!("Expected LanguageDataNotFound error"),
}
}
#[test]
fn test_validate_language_empty_string() {
let (health_checker, _temp_dir) = create_test_health_checker();
let result = health_checker.validate_language("");
assert!(result.is_err());
match result.unwrap_err() {
OcrError::LanguageDataNotFound { lang } => {
assert_eq!(lang, "");
},
_ => panic!("Expected LanguageDataNotFound error"),
}
}
#[test]
fn test_validate_language_case_sensitive() {
let (health_checker, _temp_dir) = create_test_health_checker();
// Should be case sensitive
assert!(health_checker.validate_language("eng").is_ok());
let result = health_checker.validate_language("ENG");
assert!(result.is_err());
match result.unwrap_err() {
OcrError::LanguageDataNotFound { lang } => {
assert_eq!(lang, "ENG");
},
_ => panic!("Expected LanguageDataNotFound error"),
}
}
#[test]
fn test_validate_language_with_special_characters() {
let (health_checker, _temp_dir) = create_test_health_checker();
// chi_sim contains underscore
assert!(health_checker.validate_language("chi_sim").is_ok());
// Test invalid special characters
let result = health_checker.validate_language("chi-sim");
assert!(result.is_err());
}
#[test]
fn test_validate_language_whitespace() {
let (health_checker, _temp_dir) = create_test_health_checker();
// Test with leading/trailing whitespace
let result = health_checker.validate_language(" eng ");
assert!(result.is_err());
let result = health_checker.validate_language("eng ");
assert!(result.is_err());
let result = health_checker.validate_language(" eng");
assert!(result.is_err());
}
#[test]
fn test_get_language_display_name() {
let (health_checker, _temp_dir) = create_test_health_checker();
// Test known language codes
assert_eq!(health_checker.get_language_display_name("eng"), "English");
assert_eq!(health_checker.get_language_display_name("spa"), "Spanish");
assert_eq!(health_checker.get_language_display_name("fra"), "French");
assert_eq!(health_checker.get_language_display_name("deu"), "German");
assert_eq!(health_checker.get_language_display_name("chi_sim"), "Chinese (Simplified)");
// Test unknown language code (should return the code itself)
assert_eq!(health_checker.get_language_display_name("unknown"), "unknown");
}
#[test]
fn test_concurrent_language_validation() {
use std::sync::Arc;
use std::thread;
let (health_checker, _temp_dir) = create_test_health_checker();
let health_checker = Arc::new(health_checker);
let mut handles = vec![];
// Test concurrent validation of different languages
for lang in &["eng", "spa", "fra", "deu", "chi_sim"] {
let hc = Arc::clone(&health_checker);
let lang = lang.to_string();
let handle = thread::spawn(move || {
hc.validate_language(&lang)
});
handles.push(handle);
}
// All validations should succeed
for handle in handles {
let result = handle.join().expect("Thread panicked");
assert!(result.is_ok());
}
}
#[test]
fn test_languages_alphabetically_sorted() {
let (health_checker, _temp_dir) = create_test_health_checker();
let languages = health_checker.get_available_languages().unwrap();
let mut sorted_languages = languages.clone();
sorted_languages.sort();
assert_eq!(languages, sorted_languages, "Languages should be sorted alphabetically");
}
#[test]
fn test_ignore_non_traineddata_files() {
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let tessdata_path = temp_dir.path().join("tessdata");
fs::create_dir_all(&tessdata_path).expect("Failed to create tessdata directory");
// Create mix of valid and invalid files
let files = vec![
"eng.traineddata", // Valid
"readme.txt", // Invalid - not .traineddata
"spa.traineddata", // Valid
"config.json", // Invalid - not .traineddata
"fra.backup", // Invalid - not .traineddata
"deu.traineddata", // Valid
];
for file in files {
fs::write(tessdata_path.join(file), "mock data")
.expect("Failed to create mock file");
}
let health_checker = OcrHealthChecker::new(tessdata_path);
let languages = health_checker.get_available_languages().unwrap();
// Should only include .traineddata files
assert_eq!(languages.len(), 3);
assert!(languages.contains(&"eng".to_string()));
assert!(languages.contains(&"spa".to_string()));
assert!(languages.contains(&"deu".to_string()));
}
#[test]
fn test_handle_permission_errors() {
// This test simulates permission errors by using a non-readable directory
// Note: This may not work on all systems, particularly Windows
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let tessdata_path = temp_dir.path().join("tessdata");
fs::create_dir_all(&tessdata_path).expect("Failed to create tessdata directory");
// Remove read permissions
let mut perms = fs::metadata(&tessdata_path).unwrap().permissions();
perms.set_mode(0o000);
fs::set_permissions(&tessdata_path, perms).unwrap();
let health_checker = OcrHealthChecker::new(&tessdata_path);
let result = health_checker.get_available_languages();
// Should handle permission error gracefully
assert!(result.is_err());
// Restore permissions for cleanup
let mut perms = fs::metadata(&tessdata_path).unwrap().permissions();
perms.set_mode(0o755);
fs::set_permissions(&tessdata_path, perms).unwrap();
}
}
#[test]
fn test_validate_multiple_languages_batch() {
let (health_checker, _temp_dir) = create_test_health_checker();
let languages_to_test = vec![
("eng", true),
("spa", true),
("fra", true),
("invalid", false),
("", false),
("ENG", false),
("chi_sim", true),
];
for (lang, should_be_valid) in languages_to_test {
let result = health_checker.validate_language(lang);
if should_be_valid {
assert!(result.is_ok(), "Language '{}' should be valid", lang);
} else {
assert!(result.is_err(), "Language '{}' should be invalid", lang);
}
}
}
}

1
src/ocr/tests/mod.rs Normal file
View File

@ -0,0 +1 @@
pub mod language_validation_tests;

View File

@ -0,0 +1,529 @@
use readur2::app::AppState;
use readur2::config::Config;
use readur2::db::Database;
use readur2::ocr::health::OcrHealthChecker;
use axum::http::StatusCode;
use axum_test::TestServer;
use serde_json::json;
use std::sync::Arc;
use tempfile::TempDir;
use std::fs;
use uuid::Uuid;
struct TestHarness {
server: TestServer,
_temp_dir: TempDir,
user_id: Uuid,
token: String,
}
impl TestHarness {
async fn new() -> Self {
// Create temporary directory for tessdata
let temp_dir = TempDir::new().expect("Failed to create temp directory");
let tessdata_path = temp_dir.path().join("tessdata");
fs::create_dir_all(&tessdata_path).expect("Failed to create tessdata directory");
// Create mock language files
let language_files = vec![
"eng.traineddata",
"spa.traineddata",
"fra.traineddata",
"deu.traineddata",
"ita.traineddata",
"por.traineddata",
];
for file in language_files {
fs::write(tessdata_path.join(file), "mock language data")
.expect("Failed to create mock language file");
}
// Set environment variable for tessdata path
std::env::set_var("TESSDATA_PREFIX", &tessdata_path);
// Create test database
let config = Config::from_env().expect("Failed to load config");
let db = Database::new(&config.database_url)
.await
.expect("Failed to connect to database");
// Create test user
let user_id = Uuid::new_v4();
let username = format!("testuser_{}", user_id);
let email = format!("{}@test.com", username);
sqlx::query(
"INSERT INTO users (id, username, email, password_hash) VALUES ($1, $2, $3, $4)"
)
.bind(user_id)
.bind(&username)
.bind(&email)
.bind("dummy_hash")
.execute(&db.pool)
.await
.expect("Failed to create test user");
// Create user settings
sqlx::query(
"INSERT INTO settings (user_id, ocr_language) VALUES ($1, $2)"
)
.bind(user_id)
.bind("eng")
.execute(&db.pool)
.await
.expect("Failed to create user settings");
// Create app state
let app_state = Arc::new(AppState {
db,
config,
ocr_health_checker: OcrHealthChecker::new(tessdata_path),
});
// Create test server
let app = readur2::app::create_app(app_state);
let server = TestServer::new(app).expect("Failed to create test server");
// Generate a test token (simplified for testing)
let token = format!("test_token_{}", user_id);
Self {
server,
_temp_dir: temp_dir,
user_id,
token,
}
}
async fn cleanup(&self) {
// Clean up test user
sqlx::query("DELETE FROM users WHERE id = $1")
.bind(self.user_id)
.execute(&self.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to cleanup test user");
}
}
#[tokio::test]
async fn test_get_available_languages_success() {
let harness = TestHarness::new().await;
let response = harness
.server
.get("/api/ocr/languages")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let body: serde_json::Value = response.json();
assert!(body.get("languages").is_some());
let languages = body["languages"].as_array().unwrap();
assert!(languages.len() >= 6); // We created 6 mock languages
// Check that languages have the expected structure
for lang in languages {
assert!(lang.get("code").is_some());
assert!(lang.get("name").is_some());
}
// Check that English is included
let has_english = languages.iter().any(|lang| {
lang.get("code").unwrap().as_str().unwrap() == "eng"
});
assert!(has_english);
harness.cleanup().await;
}
#[tokio::test]
async fn test_get_available_languages_unauthorized() {
let harness = TestHarness::new().await;
let response = harness
.server
.get("/api/ocr/languages")
.await;
assert_eq!(response.status_code(), StatusCode::UNAUTHORIZED);
harness.cleanup().await;
}
#[tokio::test]
async fn test_get_available_languages_includes_current_user_language() {
let harness = TestHarness::new().await;
let response = harness
.server
.get("/api/ocr/languages")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let body: serde_json::Value = response.json();
assert_eq!(body["current_user_language"].as_str().unwrap(), "eng");
harness.cleanup().await;
}
#[tokio::test]
async fn test_retry_ocr_with_language_success() {
let harness = TestHarness::new().await;
// First, create a test document
let document_id = Uuid::new_v4();
sqlx::query(
"INSERT INTO documents (id, user_id, filename, original_filename, file_size, mime_type, ocr_status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())"
)
.bind(document_id)
.bind(harness.user_id)
.bind("test.pdf")
.bind("test.pdf")
.bind(1024i64)
.bind("application/pdf")
.bind("failed")
.execute(&harness.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to create test document");
let retry_request = json!({
"language": "spa"
});
let response = harness
.server
.post(&format!("/documents/{}/retry-ocr", document_id))
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&retry_request)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let body: serde_json::Value = response.json();
assert_eq!(body["success"].as_bool().unwrap(), true);
assert!(body.get("message").is_some());
harness.cleanup().await;
}
#[tokio::test]
async fn test_retry_ocr_without_language_uses_default() {
let harness = TestHarness::new().await;
// Create a test document
let document_id = Uuid::new_v4();
sqlx::query(
"INSERT INTO documents (id, user_id, filename, original_filename, file_size, mime_type, ocr_status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())"
)
.bind(document_id)
.bind(harness.user_id)
.bind("test.pdf")
.bind("test.pdf")
.bind(1024i64)
.bind("application/pdf")
.bind("failed")
.execute(&harness.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to create test document");
let retry_request = json!({});
let response = harness
.server
.post(&format!("/documents/{}/retry-ocr", document_id))
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&retry_request)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let body: serde_json::Value = response.json();
assert_eq!(body["success"].as_bool().unwrap(), true);
harness.cleanup().await;
}
#[tokio::test]
async fn test_retry_ocr_with_invalid_language() {
let harness = TestHarness::new().await;
// Create a test document
let document_id = Uuid::new_v4();
sqlx::query(
"INSERT INTO documents (id, user_id, filename, original_filename, file_size, mime_type, ocr_status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())"
)
.bind(document_id)
.bind(harness.user_id)
.bind("test.pdf")
.bind("test.pdf")
.bind(1024i64)
.bind("application/pdf")
.bind("failed")
.execute(&harness.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to create test document");
let retry_request = json!({
"language": "invalid_lang"
});
let response = harness
.server
.post(&format!("/documents/{}/retry-ocr", document_id))
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&retry_request)
.await;
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
let body: serde_json::Value = response.json();
assert!(body.get("error").is_some());
harness.cleanup().await;
}
#[tokio::test]
async fn test_retry_ocr_nonexistent_document() {
let harness = TestHarness::new().await;
let nonexistent_id = Uuid::new_v4();
let retry_request = json!({
"language": "spa"
});
let response = harness
.server
.post(&format!("/documents/{}/retry-ocr", nonexistent_id))
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&retry_request)
.await;
assert_eq!(response.status_code(), StatusCode::NOT_FOUND);
harness.cleanup().await;
}
#[tokio::test]
async fn test_retry_ocr_unauthorized_user() {
let harness = TestHarness::new().await;
// Create a document owned by a different user
let other_user_id = Uuid::new_v4();
let document_id = Uuid::new_v4();
sqlx::query(
"INSERT INTO users (id, username, email, password_hash) VALUES ($1, $2, $3, $4)"
)
.bind(other_user_id)
.bind("otheruser")
.bind("other@test.com")
.bind("dummy_hash")
.execute(&harness.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to create other user");
sqlx::query(
"INSERT INTO documents (id, user_id, filename, original_filename, file_size, mime_type, ocr_status, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), NOW())"
)
.bind(document_id)
.bind(other_user_id)
.bind("test.pdf")
.bind("test.pdf")
.bind(1024i64)
.bind("application/pdf")
.bind("failed")
.execute(&harness.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to create test document");
let retry_request = json!({
"language": "spa"
});
let response = harness
.server
.post(&format!("/documents/{}/retry-ocr", document_id))
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&retry_request)
.await;
assert_eq!(response.status_code(), StatusCode::FORBIDDEN);
// Cleanup other user
sqlx::query("DELETE FROM users WHERE id = $1")
.bind(other_user_id)
.execute(&harness.server.into_inner().extract::<Arc<AppState>>().unwrap().db.pool)
.await
.expect("Failed to cleanup other user");
harness.cleanup().await;
}
#[tokio::test]
async fn test_document_upload_with_language_validation() {
let harness = TestHarness::new().await;
// Create a multipart form with a document and language
let file_content = b"Mock PDF content";
let form = reqwest::multipart::Form::new()
.part("file", reqwest::multipart::Part::bytes(file_content.to_vec())
.file_name("test.pdf")
.mime_str("application/pdf").unwrap())
.part("language", reqwest::multipart::Part::text("spa"));
let response = harness
.server
.post("/documents")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.multipart(form)
.await;
// Should succeed with valid language
assert_eq!(response.status_code(), StatusCode::OK);
harness.cleanup().await;
}
#[tokio::test]
async fn test_document_upload_with_invalid_language() {
let harness = TestHarness::new().await;
// Create a multipart form with invalid language
let file_content = b"Mock PDF content";
let form = reqwest::multipart::Form::new()
.part("file", reqwest::multipart::Part::bytes(file_content.to_vec())
.file_name("test.pdf")
.mime_str("application/pdf").unwrap())
.part("language", reqwest::multipart::Part::text("invalid_lang"));
let response = harness
.server
.post("/documents")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.multipart(form)
.await;
// Should fail with invalid language
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
harness.cleanup().await;
}
#[tokio::test]
async fn test_settings_update_with_ocr_language() {
let harness = TestHarness::new().await;
let settings_update = json!({
"ocrLanguage": "fra",
"concurrentOcrJobs": 2,
"ocrTimeoutSeconds": 300
});
let response = harness
.server
.put("/settings")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&settings_update)
.await;
assert_eq!(response.status_code(), StatusCode::OK);
// Verify the setting was updated
let get_response = harness
.server
.get("/settings")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.await;
assert_eq!(get_response.status_code(), StatusCode::OK);
let body: serde_json::Value = get_response.json();
assert_eq!(body["ocrLanguage"].as_str().unwrap(), "fra");
harness.cleanup().await;
}
#[tokio::test]
async fn test_settings_update_with_invalid_ocr_language() {
let harness = TestHarness::new().await;
let settings_update = json!({
"ocrLanguage": "invalid_lang",
"concurrentOcrJobs": 2
});
let response = harness
.server
.put("/settings")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.add_header("Content-Type", "application/json")
.json(&settings_update)
.await;
// Should fail with invalid language
assert_eq!(response.status_code(), StatusCode::BAD_REQUEST);
harness.cleanup().await;
}
#[tokio::test]
async fn test_ocr_health_endpoint() {
let harness = TestHarness::new().await;
let response = harness
.server
.get("/api/ocr/health")
.add_header("Authorization", &format!("Bearer {}", harness.token))
.await;
assert_eq!(response.status_code(), StatusCode::OK);
let body: serde_json::Value = response.json();
assert!(body.get("status").is_some());
assert!(body.get("available_languages").is_some());
harness.cleanup().await;
}
#[tokio::test]
async fn test_concurrent_language_requests() {
let harness = TestHarness::new().await;
// Make multiple concurrent requests to the languages endpoint
let mut handles = vec![];
for _ in 0..5 {
let server_clone = harness.server.clone();
let token_clone = harness.token.clone();
let handle = tokio::spawn(async move {
server_clone
.get("/api/ocr/languages")
.add_header("Authorization", &format!("Bearer {}", token_clone))
.await
});
handles.push(handle);
}
// All requests should succeed
for handle in handles {
let response = handle.await.expect("Task panicked");
assert_eq!(response.status_code(), StatusCode::OK);
}
harness.cleanup().await;
}