Readur/frontend/src/services/__tests__/api.schema.test.ts

293 lines
7.9 KiB
TypeScript

import { describe, test, expect } from 'vitest';
// Type definitions for API responses to ensure consistency
interface FailureCategory {
reason: string;
display_name: string;
count: number;
}
interface FailedOcrStatistics {
total_failed: number;
failure_categories: FailureCategory[];
}
interface FailedOcrResponse {
documents: any[];
pagination: {
total: number;
limit: number;
offset: number;
has_more: boolean;
};
statistics: FailedOcrStatistics;
}
describe('API Response Schema Validation', () => {
describe('FailedOcrResponse Schema', () => {
test('validates complete valid response structure', () => {
const validResponse: FailedOcrResponse = {
documents: [],
pagination: {
total: 0,
limit: 25,
offset: 0,
has_more: false,
},
statistics: {
total_failed: 0,
failure_categories: [
{
reason: 'low_ocr_confidence',
display_name: 'Low OCR Confidence',
count: 5,
},
{
reason: 'pdf_parsing_error',
display_name: 'PDF Parsing Error',
count: 2,
},
],
},
};
expect(validateFailedOcrResponse(validResponse)).toBe(true);
});
test('validates response with empty failure_categories', () => {
const responseWithEmptyCategories: FailedOcrResponse = {
documents: [],
pagination: {
total: 0,
limit: 25,
offset: 0,
has_more: false,
},
statistics: {
total_failed: 0,
failure_categories: [],
},
};
expect(validateFailedOcrResponse(responseWithEmptyCategories)).toBe(true);
});
test('catches missing required fields', () => {
const invalidResponses = [
// Missing documents
{
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: { total_failed: 0, failure_categories: [] },
},
// Missing pagination
{
documents: [],
statistics: { total_failed: 0, failure_categories: [] },
},
// Missing statistics
{
documents: [],
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
},
// Missing statistics.failure_categories
{
documents: [],
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: { total_failed: 0 },
},
];
for (const invalidResponse of invalidResponses) {
expect(validateFailedOcrResponse(invalidResponse as any)).toBe(false);
}
});
test('catches null/undefined critical fields', () => {
const nullFieldResponses = [
{
documents: [],
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: null, // This was our original bug
},
{
documents: [],
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: {
total_failed: 0,
failure_categories: null, // This could also cause issues
},
},
{
documents: null,
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: { total_failed: 0, failure_categories: [] },
},
];
for (const nullResponse of nullFieldResponses) {
expect(validateFailedOcrResponse(nullResponse as any)).toBe(false);
}
});
test('validates failure category structure', () => {
const invalidCategoryStructures = [
// Missing required fields in category
{
documents: [],
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: {
total_failed: 1,
failure_categories: [
{ reason: 'test', count: 1 }, // Missing display_name
],
},
},
// Wrong type for count
{
documents: [],
pagination: { total: 0, limit: 25, offset: 0, has_more: false },
statistics: {
total_failed: 1,
failure_categories: [
{ reason: 'test', display_name: 'Test', count: 'not a number' },
],
},
},
];
for (const invalidStructure of invalidCategoryStructures) {
expect(validateFailedOcrResponse(invalidStructure as any)).toBe(false);
}
});
});
describe('Frontend Safety Helpers', () => {
test('safe array access helper works correctly', () => {
const responses = [
{ failure_categories: [{ reason: 'test', display_name: 'Test', count: 1 }] },
{ failure_categories: [] },
{ failure_categories: null },
{ failure_categories: undefined },
{},
null,
undefined,
];
for (const response of responses) {
const result = safeGetFailureCategories(response);
expect(Array.isArray(result)).toBe(true);
expect(result.length).toBeGreaterThanOrEqual(0);
}
});
test('safe statistics access helper works correctly', () => {
const responses = [
{ statistics: { total_failed: 5, failure_categories: [] } },
{ statistics: null },
{ statistics: undefined },
{},
null,
undefined,
];
for (const response of responses) {
const result = safeGetStatistics(response);
expect(typeof result.total_failed).toBe('number');
expect(Array.isArray(result.failure_categories)).toBe(true);
}
});
});
});
// Validation functions that could be used in production code
function validateFailedOcrResponse(response: any): response is FailedOcrResponse {
if (!response || typeof response !== 'object') {
return false;
}
// Check required top-level fields
if (!Array.isArray(response.documents)) {
return false;
}
if (!response.pagination || typeof response.pagination !== 'object') {
return false;
}
if (!response.statistics || typeof response.statistics !== 'object') {
return false;
}
// Check pagination structure
const { pagination } = response;
if (
typeof pagination.total !== 'number' ||
typeof pagination.limit !== 'number' ||
typeof pagination.offset !== 'number' ||
typeof pagination.has_more !== 'boolean'
) {
return false;
}
// Check statistics structure
const { statistics } = response;
if (
typeof statistics.total_failed !== 'number' ||
!Array.isArray(statistics.failure_categories)
) {
return false;
}
// Check each failure category structure
for (const category of statistics.failure_categories) {
if (
!category ||
typeof category.reason !== 'string' ||
typeof category.display_name !== 'string' ||
typeof category.count !== 'number'
) {
return false;
}
}
return true;
}
// Helper functions for safe access (these could be used in components)
function safeGetFailureCategories(response: any): FailureCategory[] {
if (
response &&
response.statistics &&
Array.isArray(response.statistics.failure_categories)
) {
return response.statistics.failure_categories;
}
return [];
}
function safeGetStatistics(response: any): FailedOcrStatistics {
const defaultStats: FailedOcrStatistics = {
total_failed: 0,
failure_categories: [],
};
if (
response &&
response.statistics &&
typeof response.statistics === 'object'
) {
return {
total_failed: typeof response.statistics.total_failed === 'number'
? response.statistics.total_failed
: 0,
failure_categories: Array.isArray(response.statistics.failure_categories)
? response.statistics.failure_categories
: [],
};
}
return defaultStats;
}
// Export helpers for use in production code
export { validateFailedOcrResponse, safeGetFailureCategories, safeGetStatistics };