einvoice/test/test.error-handling.ts

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('&amp;');
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();