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: 'XML' }); 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 = '\ufeff123'; 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 = 'Smith & Jones Ltd'; 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 = ` TEST-001 `; 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 = {}; 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: '123', expectedRecovery: false // Hard to recover automatically }, { name: 'Extra whitespace', xml: ' \n\n 123', expectedRecovery: true }, { name: 'Wrong encoding declaration', xml: '123', 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, byCode: {} as Record, 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();