394 lines
13 KiB
TypeScript
394 lines
13 KiB
TypeScript
import { tap, expect } from '@push.rocks/tapbundle';
|
|
import {
|
|
EInvoice,
|
|
EInvoiceError,
|
|
EInvoiceParsingError,
|
|
EInvoiceValidationError,
|
|
EInvoicePDFError,
|
|
EInvoiceFormatError,
|
|
ErrorRecovery,
|
|
ErrorContext
|
|
} from '../ts/index.js';
|
|
import { ValidationLevel } from '../ts/interfaces/common.js';
|
|
import { TestFileHelpers, TestFileCategories } from './test-utils.js';
|
|
import * as path from 'path';
|
|
|
|
/**
|
|
* Error handling and recovery test suite
|
|
*/
|
|
|
|
// Test EInvoiceParsingError functionality
|
|
tap.test('Error Handling - Parsing errors with location info', async () => {
|
|
const malformedXml = `<?xml version="1.0"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>123</ID>
|
|
<IssueDate>2024-01-01
|
|
<InvoiceLine>
|
|
<ID>1</ID>
|
|
</InvoiceLine>
|
|
</Invoice>`;
|
|
|
|
try {
|
|
await EInvoice.fromXml(malformedXml);
|
|
expect.fail('Should have thrown a parsing error');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(EInvoiceParsingError);
|
|
|
|
if (error instanceof EInvoiceParsingError) {
|
|
console.log('✓ Parsing error caught correctly');
|
|
console.log(` Message: ${error.message}`);
|
|
console.log(` Code: ${error.code}`);
|
|
console.log(` Detailed: ${error.getDetailedMessage()}`);
|
|
|
|
// Check error properties
|
|
expect(error.code).toEqual('PARSE_ERROR');
|
|
expect(error.name).toEqual('EInvoiceParsingError');
|
|
expect(error.details).toBeTruthy();
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test XML recovery mechanisms
|
|
tap.test('Error Handling - XML recovery for common issues', async () => {
|
|
// Test 1: XML with BOM
|
|
const xmlWithBOM = '\ufeff<?xml version="1.0"?><Invoice><ID>123</ID></Invoice>';
|
|
const bomError = new EInvoiceParsingError('BOM detected', { xmlSnippet: xmlWithBOM.substring(0, 50) });
|
|
|
|
const bomRecovery = await ErrorRecovery.attemptXMLRecovery(xmlWithBOM, bomError);
|
|
expect(bomRecovery.success).toBeTruthy();
|
|
expect(bomRecovery.cleanedXml).toBeTruthy();
|
|
expect(bomRecovery.cleanedXml!.charCodeAt(0)).not.toEqual(0xFEFF);
|
|
console.log('✓ BOM removal recovery successful');
|
|
|
|
// Test 2: Unescaped ampersands
|
|
const xmlWithAmpersand = '<?xml version="1.0"?><Invoice><Name>Smith & Jones Ltd</Name></Invoice>';
|
|
const ampError = new EInvoiceParsingError('Unescaped ampersand', {});
|
|
|
|
const ampRecovery = await ErrorRecovery.attemptXMLRecovery(xmlWithAmpersand, ampError);
|
|
expect(ampRecovery.success).toBeTruthy();
|
|
if (ampRecovery.cleanedXml) {
|
|
expect(ampRecovery.cleanedXml).toInclude('&');
|
|
console.log('✓ Ampersand escaping recovery successful');
|
|
}
|
|
});
|
|
|
|
// Test validation error handling
|
|
tap.test('Error Handling - Validation errors with detailed reports', async () => {
|
|
const invoice = new EInvoice();
|
|
|
|
try {
|
|
await invoice.validate(ValidationLevel.BUSINESS);
|
|
expect.fail('Should have thrown validation error for empty invoice');
|
|
} catch (error) {
|
|
expect(error).toBeInstanceOf(EInvoiceValidationError);
|
|
|
|
if (error instanceof EInvoiceValidationError) {
|
|
console.log('✓ Validation error caught');
|
|
console.log('Validation Report:');
|
|
console.log(error.getValidationReport());
|
|
|
|
// Check error filtering
|
|
const errors = error.getErrorsBySeverity('error');
|
|
expect(errors.length).toBeGreaterThan(0);
|
|
|
|
const warnings = error.getErrorsBySeverity('warning');
|
|
console.log(` Errors: ${errors.length}, Warnings: ${warnings.length}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test PDF error handling
|
|
tap.test('Error Handling - PDF operation errors', async () => {
|
|
// Test extraction error
|
|
const extractError = new EInvoicePDFError(
|
|
'No XML found in PDF',
|
|
'extract',
|
|
{
|
|
pdfInfo: {
|
|
filename: 'test.pdf',
|
|
size: 1024 * 1024,
|
|
pageCount: 10
|
|
}
|
|
}
|
|
);
|
|
|
|
console.log('PDF Extraction Error:');
|
|
console.log(` Message: ${extractError.message}`);
|
|
console.log(` Operation: ${extractError.operation}`);
|
|
console.log(' Recovery suggestions:');
|
|
extractError.getRecoverySuggestions().forEach(s => console.log(` - ${s}`));
|
|
|
|
expect(extractError.code).toEqual('PDF_EXTRACT_ERROR');
|
|
expect(extractError.getRecoverySuggestions().length).toBeGreaterThan(0);
|
|
|
|
// Test embed error
|
|
const embedError = new EInvoicePDFError(
|
|
'Failed to embed XML',
|
|
'embed',
|
|
{ xmlLength: 50000 }
|
|
);
|
|
|
|
expect(embedError.code).toEqual('PDF_EMBED_ERROR');
|
|
expect(embedError.getRecoverySuggestions()).toContain('Try with a smaller XML payload');
|
|
});
|
|
|
|
// Test format errors
|
|
tap.test('Error Handling - Format conversion errors', async () => {
|
|
const formatError = new EInvoiceFormatError(
|
|
'Cannot convert invoice: incompatible fields',
|
|
{
|
|
sourceFormat: 'fatturapa',
|
|
targetFormat: 'xrechnung',
|
|
unsupportedFeatures: [
|
|
'Italian-specific tax codes',
|
|
'PEC electronic address format',
|
|
'Bollo virtuale'
|
|
]
|
|
}
|
|
);
|
|
|
|
console.log('Format Conversion Error:');
|
|
console.log(formatError.getCompatibilityReport());
|
|
|
|
expect(formatError.sourceFormat).toEqual('fatturapa');
|
|
expect(formatError.targetFormat).toEqual('xrechnung');
|
|
expect(formatError.unsupportedFeatures?.length).toEqual(3);
|
|
});
|
|
|
|
// Test error context builder
|
|
tap.test('Error Handling - Error context enrichment', async () => {
|
|
const context = new ErrorContext()
|
|
.add('operation', 'invoice_validation')
|
|
.add('invoiceId', 'INV-2024-001')
|
|
.add('format', 'facturx')
|
|
.addTimestamp()
|
|
.addEnvironment()
|
|
.build();
|
|
|
|
expect(context.operation).toEqual('invoice_validation');
|
|
expect(context.invoiceId).toEqual('INV-2024-001');
|
|
expect(context.timestamp).toBeTruthy();
|
|
expect(context.environment).toBeTruthy();
|
|
expect(context.environment.nodeVersion).toBeTruthy();
|
|
|
|
console.log('✓ Error context built successfully');
|
|
console.log(` Context keys: ${Object.keys(context).join(', ')}`);
|
|
});
|
|
|
|
// Test error propagation through the stack
|
|
tap.test('Error Handling - Error propagation and chaining', async () => {
|
|
// Create a chain of errors
|
|
const rootCause = new Error('Database connection failed');
|
|
const serviceError = new EInvoiceError(
|
|
'Failed to load invoice template',
|
|
'TEMPLATE_ERROR',
|
|
{ templateId: 'facturx-v2' },
|
|
rootCause
|
|
);
|
|
const userError = new EInvoicePDFError(
|
|
'Cannot generate PDF invoice',
|
|
'create',
|
|
{ invoiceId: 'INV-001' },
|
|
serviceError
|
|
);
|
|
|
|
console.log('Error Chain:');
|
|
console.log(` User sees: ${userError.message}`);
|
|
console.log(` Caused by: ${userError.cause?.message}`);
|
|
console.log(` Root cause: ${(userError.cause as EInvoiceError)?.cause?.message}`);
|
|
|
|
expect(userError.cause).toBeTruthy();
|
|
expect((userError.cause as EInvoiceError).cause).toBeTruthy();
|
|
});
|
|
|
|
// Test recovery from real corpus errors
|
|
tap.test('Error Handling - Recovery from corpus parsing errors', async () => {
|
|
// Try to load files that might have issues
|
|
const problematicFiles = [
|
|
'test/assets/corpus/other/eicar.cii.xml',
|
|
'test/assets/corpus/other/eicar.ubl.xml'
|
|
];
|
|
|
|
for (const filePath of problematicFiles) {
|
|
try {
|
|
const fileBuffer = await TestFileHelpers.loadTestFile(filePath);
|
|
const xmlString = fileBuffer.toString('utf-8');
|
|
|
|
const invoice = await EInvoice.fromXml(xmlString);
|
|
console.log(`○ ${path.basename(filePath)}: Loaded successfully (no error to handle)`);
|
|
|
|
} catch (error) {
|
|
if (error instanceof EInvoiceParsingError) {
|
|
console.log(`✓ ${path.basename(filePath)}: Parsing error handled`);
|
|
|
|
// Attempt recovery
|
|
const recovery = await ErrorRecovery.attemptXMLRecovery(
|
|
error.details?.xmlString || '',
|
|
error
|
|
);
|
|
|
|
if (recovery.success) {
|
|
console.log(` Recovery: ${recovery.message}`);
|
|
}
|
|
} else {
|
|
console.log(`✓ ${path.basename(filePath)}: Error handled - ${error.message}`);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test concurrent error scenarios
|
|
tap.test('Error Handling - Concurrent error handling', async () => {
|
|
const errorScenarios = [
|
|
async () => {
|
|
throw new EInvoiceParsingError('Scenario 1: Invalid XML', { line: 10, column: 5 });
|
|
},
|
|
async () => {
|
|
throw new EInvoiceValidationError('Scenario 2: Validation failed', [
|
|
{ code: 'BR-01', message: 'Invoice number required' }
|
|
]);
|
|
},
|
|
async () => {
|
|
throw new EInvoicePDFError('Scenario 3: PDF corrupted', 'extract');
|
|
},
|
|
async () => {
|
|
throw new EInvoiceFormatError('Scenario 4: Format unsupported', {
|
|
sourceFormat: 'custom',
|
|
targetFormat: 'xrechnung'
|
|
});
|
|
}
|
|
];
|
|
|
|
const results = await Promise.allSettled(errorScenarios.map(fn => fn()));
|
|
|
|
let errorTypeCounts: Record<string, number> = {};
|
|
|
|
results.forEach((result, index) => {
|
|
if (result.status === 'rejected') {
|
|
const errorType = result.reason.constructor.name;
|
|
errorTypeCounts[errorType] = (errorTypeCounts[errorType] || 0) + 1;
|
|
console.log(`✓ Scenario ${index + 1}: ${errorType} handled`);
|
|
}
|
|
});
|
|
|
|
expect(Object.keys(errorTypeCounts).length).toEqual(4);
|
|
console.log('\nError type distribution:', errorTypeCounts);
|
|
});
|
|
|
|
// Test error serialization for logging
|
|
tap.test('Error Handling - Error serialization', async () => {
|
|
const error = new EInvoiceValidationError(
|
|
'Multiple validation failures',
|
|
[
|
|
{ code: 'BR-01', message: 'Invoice number missing', location: '/Invoice/ID' },
|
|
{ code: 'BR-05', message: 'Invalid date format', location: '/Invoice/IssueDate' },
|
|
{ code: 'BR-CL-01', message: 'Invalid currency code', location: '/Invoice/DocumentCurrencyCode' }
|
|
],
|
|
{ invoiceId: 'TEST-001', validationLevel: 'BUSINESS' }
|
|
);
|
|
|
|
// Test JSON serialization
|
|
const serialized = JSON.stringify({
|
|
name: error.name,
|
|
message: error.message,
|
|
code: error.code,
|
|
validationErrors: error.validationErrors,
|
|
details: error.details
|
|
}, null, 2);
|
|
|
|
const parsed = JSON.parse(serialized);
|
|
expect(parsed.name).toEqual('EInvoiceValidationError');
|
|
expect(parsed.code).toEqual('VALIDATION_ERROR');
|
|
expect(parsed.validationErrors.length).toEqual(3);
|
|
|
|
console.log('✓ Error serializes correctly for logging');
|
|
console.log('Serialized error sample:');
|
|
console.log(serialized.substring(0, 200) + '...');
|
|
});
|
|
|
|
// Test error recovery strategies
|
|
tap.test('Error Handling - Recovery strategy selection', async () => {
|
|
// Simulate different error scenarios and recovery strategies
|
|
const scenarios = [
|
|
{
|
|
name: 'Missing closing tag',
|
|
xml: '<?xml version="1.0"?><Invoice><ID>123</ID>',
|
|
expectedRecovery: false // Hard to recover automatically
|
|
},
|
|
{
|
|
name: 'Extra whitespace',
|
|
xml: '<?xml version="1.0"?> \n\n <Invoice><ID>123</ID></Invoice>',
|
|
expectedRecovery: true
|
|
},
|
|
{
|
|
name: 'Wrong encoding declaration',
|
|
xml: '<?xml version="1.0" encoding="UTF-16"?><Invoice><ID>123</ID></Invoice>',
|
|
expectedRecovery: true
|
|
}
|
|
];
|
|
|
|
for (const scenario of scenarios) {
|
|
try {
|
|
await EInvoice.fromXml(scenario.xml);
|
|
console.log(`○ ${scenario.name}: No error occurred`);
|
|
} catch (error) {
|
|
if (error instanceof EInvoiceParsingError) {
|
|
const recovery = await ErrorRecovery.attemptXMLRecovery(scenario.xml, error);
|
|
const result = recovery.success ? '✓' : '✗';
|
|
console.log(`${result} ${scenario.name}: Recovery ${recovery.success ? 'succeeded' : 'failed'}`);
|
|
|
|
if (scenario.expectedRecovery !== recovery.success) {
|
|
console.log(` Note: Expected recovery=${scenario.expectedRecovery}, got=${recovery.success}`);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test error metrics collection
|
|
tap.test('Error Handling - Error metrics and patterns', async () => {
|
|
const errorMetrics = {
|
|
total: 0,
|
|
byType: {} as Record<string, number>,
|
|
byCode: {} as Record<string, number>,
|
|
recoveryAttempts: 0,
|
|
recoverySuccesses: 0
|
|
};
|
|
|
|
// Simulate processing multiple files
|
|
const testFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_UBL_INVOICE, 'BR-*.xml');
|
|
|
|
for (const file of testFiles.slice(0, 10)) {
|
|
try {
|
|
const buffer = await TestFileHelpers.loadTestFile(file);
|
|
const invoice = await EInvoice.fromXml(buffer.toString('utf-8'));
|
|
await invoice.validate(ValidationLevel.BUSINESS);
|
|
} catch (error) {
|
|
errorMetrics.total++;
|
|
|
|
if (error instanceof EInvoiceError) {
|
|
const type = error.constructor.name;
|
|
errorMetrics.byType[type] = (errorMetrics.byType[type] || 0) + 1;
|
|
errorMetrics.byCode[error.code] = (errorMetrics.byCode[error.code] || 0) + 1;
|
|
|
|
// Try recovery for parsing errors
|
|
if (error instanceof EInvoiceParsingError) {
|
|
errorMetrics.recoveryAttempts++;
|
|
const recovery = await ErrorRecovery.attemptXMLRecovery('', error);
|
|
if (recovery.success) {
|
|
errorMetrics.recoverySuccesses++;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
console.log('\nError Metrics Summary:');
|
|
console.log(` Total errors: ${errorMetrics.total}`);
|
|
console.log(` Error types:`, errorMetrics.byType);
|
|
console.log(` Recovery rate: ${errorMetrics.recoveryAttempts > 0
|
|
? (errorMetrics.recoverySuccesses / errorMetrics.recoveryAttempts * 100).toFixed(1)
|
|
: 0}%`);
|
|
});
|
|
|
|
tap.start(); |