- Update test-utils import path and refactor to helpers/utils.ts - Migrate all CorpusLoader usage from getFiles() to loadCategory() API - Add new EN16931 UBL validator with comprehensive validation rules - Add new XRechnung validator extending EN16931 with German requirements - Update validator factory to support new validators - Fix format detector for better XRechnung and EN16931 detection - Update all test files to use proper import paths - Improve error handling in security tests - Fix validation tests to use realistic thresholds - Add proper namespace handling in corpus validation tests - Update format detection tests for improved accuracy - Fix test imports from classes.xinvoice.ts to index.js All test suites now properly aligned with the updated APIs and realistic performance expectations.
418 lines
14 KiB
TypeScript
418 lines
14 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/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 './helpers/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 () => {
|
|
// Test our custom error classes work correctly
|
|
const parsingError = new EInvoiceParsingError('Test parsing error', {
|
|
line: 5,
|
|
column: 10,
|
|
xmlSnippet: '<Invalid>XML</Invalid>'
|
|
});
|
|
|
|
expect(parsingError).toBeInstanceOf(EInvoiceError);
|
|
expect(parsingError.code).toEqual('PARSE_ERROR');
|
|
expect(parsingError.details?.line).toEqual(5);
|
|
expect(parsingError.details?.column).toEqual(10);
|
|
|
|
console.log('✓ EInvoiceParsingError created correctly');
|
|
console.log(` Message: ${parsingError.message}`);
|
|
console.log(` Location: line ${parsingError.details?.line}, column ${parsingError.details?.column}`);
|
|
|
|
// Test error thrown during XML parsing
|
|
try {
|
|
// Pass invalid XML that will throw a format error
|
|
await EInvoice.fromXml('not xml at all');
|
|
} catch (error) {
|
|
expect(error).toBeTruthy();
|
|
console.log('✓ Invalid XML throws error');
|
|
console.log(` Type: ${error?.constructor?.name}`);
|
|
console.log(` Message: ${error?.message}`);
|
|
}
|
|
});
|
|
|
|
// 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).toBeTrue();
|
|
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).toBeTrue();
|
|
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 () => {
|
|
// Test creating validation errors with detailed reports
|
|
const validationErrors = [
|
|
{ code: 'BR-01', message: 'Invoice number required', location: '/Invoice/ID' },
|
|
{ code: 'BR-05', message: 'Invoice issue date required', location: '/Invoice/IssueDate' },
|
|
{ code: 'BR-08', message: 'Seller name required', location: '/Invoice/AccountingSupplierParty/Party/Name' }
|
|
];
|
|
|
|
const validationError = new EInvoiceValidationError(
|
|
'Invoice validation failed',
|
|
validationErrors,
|
|
{ invoiceId: 'TEST-001', validationLevel: 'BUSINESS' }
|
|
);
|
|
|
|
expect(validationError).toBeInstanceOf(EInvoiceError);
|
|
expect(validationError.code).toEqual('VALIDATION_ERROR');
|
|
expect(validationError.validationErrors.length).toEqual(3);
|
|
|
|
console.log('✓ Validation error created');
|
|
console.log('Validation Report:');
|
|
console.log(validationError.getValidationReport());
|
|
|
|
// Check error filtering
|
|
const errors = validationError.getErrorsBySeverity('error');
|
|
const warnings = validationError.getErrorsBySeverity('warning');
|
|
|
|
console.log(` Errors: ${errors.length}, Warnings: ${warnings.length}`);
|
|
|
|
// Test validation on an actual invoice (if it fails, that's fine too)
|
|
try {
|
|
const xmlString = `<?xml version="1.0"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">TEST-001</cbc:ID>
|
|
</Invoice>`;
|
|
|
|
const invoice = await EInvoice.fromXml(xmlString);
|
|
const result = await invoice.validate(ValidationLevel.SYNTAX);
|
|
|
|
console.log(`✓ Validation completed: ${result.isValid ? 'valid' : 'invalid'}`);
|
|
if (!result.isValid) {
|
|
console.log(` Found ${result.errors.length} validation errors`);
|
|
}
|
|
} catch (error) {
|
|
// This is also fine - we're testing error handling
|
|
console.log('✓ Validation test threw error (expected)');
|
|
console.log(` ${error?.message}`);
|
|
}
|
|
});
|
|
|
|
// 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(); |