Readur/frontend/src/services/__tests__/api-ocr.test.ts

335 lines
9.2 KiB
TypeScript

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);
});
});
});