293 lines
7.9 KiB
TypeScript
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 }; |