import { tap, expect } from '@git.zone/tstest/tapbundle'; import { EInvoice, EInvoiceValidationError } from '../ts/index.js'; import { ValidationLevel, InvoiceFormat } from '../ts/interfaces/common.js'; import { TestFileHelpers, TestFileCategories, InvoiceAssertions, PerformanceUtils } from './helpers/utils.js'; import * as plugins from '../ts/plugins.js'; /** * Comprehensive validation test suite using EN16931 test cases */ // Test Business Rule validations from EN16931 tap.test('Validation Suite - EN16931 Business Rules (BR-*)', async () => { const testFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_UBL_INVOICE, 'BR-*.xml'); console.log(`Testing ${testFiles.length} Business Rule validation files`); const results = { passed: 0, failed: 0, errors: [] as string[] }; for (const file of testFiles) { const fileName = plugins.path.basename(file); const shouldFail = fileName.includes('BR-'); // These files test specific BR violations try { const xmlBuffer = await TestFileHelpers.loadTestFile(file); let xmlString = xmlBuffer.toString('utf-8'); // These test files wrap the invoice in a testSet element // Extract the invoice content if it's a test wrapper if (xmlString.includes(']*>[\s\S]*?<\/Invoice>/); if (invoiceMatch) { // Add proper namespaces to make it a valid UBL invoice xmlString = ` ${invoiceMatch[0]}`; } else { console.log(`✗ ${fileName}: No Invoice element found in test file`); results.failed++; continue; } } const einvoice = await EInvoice.fromXml(xmlString); const { result: validation, duration } = await PerformanceUtils.measure( 'br-validation', async () => einvoice.validate(ValidationLevel.BUSINESS) ); // Most BR-*.xml files are designed to fail specific business rules if (shouldFail && !validation.valid) { results.passed++; console.log(`✓ ${fileName}: Correctly failed validation (${duration.toFixed(2)}ms)`); // Check that the correct BR code is in the errors const brCode = fileName.match(/BR-\d+/)?.[0]; if (brCode) { const hasCorrectError = validation.errors.some(e => e.code.includes(brCode)); if (!hasCorrectError) { console.log(` ⚠ Expected error code ${brCode} not found in: ${validation.errors.map(e => e.code).join(', ')}`); } } } else if (!shouldFail && validation.valid) { results.passed++; console.log(`✓ ${fileName}: Correctly passed validation (${duration.toFixed(2)}ms)`); } else { results.failed++; results.errors.push(`${fileName}: Unexpected result - valid: ${validation.valid}`); console.log(`✗ ${fileName}: Unexpected validation result`); if (validation.errors.length > 0) { console.log(` Errors: ${validation.errors.map(e => `${e.code}: ${e.message}`).join('; ')}`); } } } catch (error) { results.failed++; results.errors.push(`${fileName}: ${error.message}`); console.log(`✗ ${fileName}: Error - ${error.message}`); } } console.log(`\nBusiness Rules Summary: ${results.passed} passed, ${results.failed} failed`); if (results.errors.length > 0) { console.log('Failures:', results.errors); } // Allow some failures as not all validators may be implemented expect(results.passed).toBeGreaterThan(0); }); // Test Codelist validations tap.test('Validation Suite - EN16931 Codelist validations (BR-CL-*)', async () => { const testFiles = await TestFileHelpers.getTestFiles(TestFileCategories.EN16931_UBL_INVOICE, 'BR-CL-*.xml'); console.log(`Testing ${testFiles.length} Codelist validation files`); let validatedCount = 0; for (const file of testFiles.slice(0, 10)) { // Test first 10 for speed const fileName = plugins.path.basename(file); try { const xmlBuffer = await TestFileHelpers.loadTestFile(file); let xmlString = xmlBuffer.toString('utf-8'); // These test files wrap the invoice in a testSet element // Extract the invoice content if it's a test wrapper if (xmlString.includes(']*>[\s\S]*?<\/Invoice>/); if (invoiceMatch) { xmlString = ` ${invoiceMatch[0]}`; } else { console.log(`✗ ${fileName}: No Invoice element found in test file`); continue; } } const einvoice = await EInvoice.fromXml(xmlString); const validation = await einvoice.validate(ValidationLevel.SEMANTIC); validatedCount++; // These files test invalid code values if (!validation.valid) { const clCode = fileName.match(/BR-CL-\d+/)?.[0]; console.log(`✓ ${fileName}: Detected invalid code (${clCode})`); } else { console.log(`○ ${fileName}: Validation passed (may need stricter codelist checking)`); } } catch (error) { console.log(`✗ ${fileName}: Error - ${error.message}`); } } expect(validatedCount).toBeGreaterThan(0); console.log(`Validated ${validatedCount} codelist test files`); }); // Test syntax validation tap.test('Validation Suite - Syntax validation levels', async () => { const xmlWithError = ` 123 not-a-date This element doesn't belong here `; const einvoice = new EInvoice(); // Test that we can catch parsing errors try { await einvoice.loadXml(xmlWithError); // Syntax validation should catch schema violations const syntaxValidation = await einvoice.validate(ValidationLevel.SYNTAX); console.log('Syntax validation:', syntaxValidation.valid ? 'PASSED' : 'FAILED'); if (!syntaxValidation.valid) { console.log('Syntax errors found:', syntaxValidation.errors.length); syntaxValidation.errors.forEach(err => { console.log(` - ${err.code}: ${err.message}`); }); } } catch (error) { if (error instanceof EInvoiceValidationError) { console.log('✓ Validation error caught correctly'); console.log(error.getValidationReport()); } } }); // Test validation error reporting tap.test('Validation Suite - Error reporting and recovery', async () => { const testInvoice = new EInvoice(); // Try to validate without loading XML try { await testInvoice.validate(); } catch (error) { expect(error).toBeInstanceOf(EInvoiceValidationError); if (error instanceof EInvoiceValidationError) { // The error might be "Cannot validate: format unknown" since no XML is loaded console.log('✓ Empty invoice validation error handled correctly'); console.log(` Error: ${error.message}`); } } // Test with minimal valid invoice testInvoice.id = 'TEST-001'; testInvoice.invoiceId = 'INV-001'; testInvoice.from.name = 'Test Seller'; testInvoice.to.name = 'Test Buyer'; testInvoice.items = [{ position: 1, name: 'Test Item', unitType: 'EA', unitQuantity: 1, unitNetPrice: 100, vatPercentage: 19 }]; // This should fail because we don't have XML loaded try { await testInvoice.validate(); } catch (error) { expect(error).toBeInstanceOf(EInvoiceValidationError); console.log('✓ Validation requires loaded XML'); } }); // Test format-specific validation tap.test('Validation Suite - Format-specific validation rules', async () => { // Test XRechnung specific validation const xrechnungFiles = await TestFileHelpers.getTestFiles( TestFileCategories.CII_XMLRECHNUNG, 'XRECHNUNG_*.xml' ); if (xrechnungFiles.length > 0) { console.log(`Testing ${xrechnungFiles.length} XRechnung files`); for (const file of xrechnungFiles.slice(0, 3)) { const xmlBuffer = await TestFileHelpers.loadTestFile(file); const einvoice = await EInvoice.fromXml(xmlBuffer.toString('utf-8')); const validation = await einvoice.validate(ValidationLevel.BUSINESS); console.log(`${plugins.path.basename(file)}: ${validation.valid ? 'VALID' : 'INVALID'}`); if (!validation.valid && validation.errors.length > 0) { console.log(` First error: ${validation.errors[0].code} - ${validation.errors[0].message}`); } } } // Test ZUGFeRD specific validation console.log('\nTesting ZUGFeRD profile validation:'); const zugferdPdfs = await TestFileHelpers.getTestFiles( TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf' ); for (const file of zugferdPdfs.slice(0, 2)) { try { const pdfBuffer = await TestFileHelpers.loadTestFile(file); const einvoice = await EInvoice.fromPdf(pdfBuffer); // Check which ZUGFeRD profile is used const format = einvoice.getFormat(); console.log(`${plugins.path.basename(file)}: Format ${format}`); // Validate according to profile const validation = await einvoice.validate(ValidationLevel.SEMANTIC); console.log(` Validation: ${validation.valid ? 'VALID' : 'INVALID'}`); } catch (error) { console.log(`${plugins.path.basename(file)}: Skipped - ${error.message}`); } } }); // Test validation performance tap.test('Validation Suite - Performance benchmarks', async () => { const files = await TestFileHelpers.getTestFiles( TestFileCategories.UBL_XMLRECHNUNG, '*.xml' ); if (files.length > 0) { const xmlBuffer = await TestFileHelpers.loadTestFile(files[0]); const xmlString = xmlBuffer.toString('utf-8'); const einvoice = await EInvoice.fromXml(xmlString); // Benchmark different validation levels console.log('\nValidation Performance:'); // Syntax validation const syntaxTimes: number[] = []; for (let i = 0; i < 10; i++) { const { duration } = await PerformanceUtils.measure( 'syntax-validation', async () => einvoice.validate(ValidationLevel.SYNTAX) ); syntaxTimes.push(duration); } const avgSyntax = syntaxTimes.reduce((a, b) => a + b) / syntaxTimes.length; console.log(`Syntax validation: avg ${avgSyntax.toFixed(2)}ms`); // Semantic validation const semanticTimes: number[] = []; for (let i = 0; i < 10; i++) { const { duration } = await PerformanceUtils.measure( 'semantic-validation', async () => einvoice.validate(ValidationLevel.SEMANTIC) ); semanticTimes.push(duration); } const avgSemantic = semanticTimes.reduce((a, b) => a + b) / semanticTimes.length; console.log(`Semantic validation: avg ${avgSemantic.toFixed(2)}ms`); // Business validation const businessTimes: number[] = []; for (let i = 0; i < 10; i++) { const { duration } = await PerformanceUtils.measure( 'business-validation', async () => einvoice.validate(ValidationLevel.BUSINESS) ); businessTimes.push(duration); } const avgBusiness = businessTimes.reduce((a, b) => a + b) / businessTimes.length; console.log(`Business validation: avg ${avgBusiness.toFixed(2)}ms`); // Validation should get progressively slower with higher levels expect(avgSyntax).toBeLessThan(avgSemantic); expect(avgSemantic).toBeLessThan(avgBusiness); } }); // Test calculation validations tap.test('Validation Suite - Calculation and sum validations', async () => { const einvoice = new EInvoice(); einvoice.id = 'CALC-TEST-001'; einvoice.invoiceId = 'CALC-001'; einvoice.from.name = 'Calculator Corp'; einvoice.to.name = 'Number Inc'; // Add items with specific calculations einvoice.items = [ { position: 1, name: 'Product A', unitType: 'EA', unitQuantity: 5, unitNetPrice: 100, // Total: 500 vatPercentage: 19 // VAT: 95 }, { position: 2, name: 'Product B', unitType: 'EA', unitQuantity: 3, unitNetPrice: 50, // Total: 150 vatPercentage: 19 // VAT: 28.50 } ]; // Expected totals: // Net: 650 // VAT: 123.50 // Gross: 773.50 // Generate XML and validate try { const xml = await einvoice.exportXml('facturx'); await einvoice.loadXml(xml); const validation = await einvoice.validate(ValidationLevel.BUSINESS); if (!validation.valid) { const calcErrors = validation.errors.filter(e => e.code.includes('BR-CO') || e.message.toLowerCase().includes('calc') ); if (calcErrors.length > 0) { console.log('Calculation validation errors found:'); calcErrors.forEach(err => { console.log(` - ${err.code}: ${err.message}`); }); } } else { console.log('✓ Invoice calculations validated successfully'); } } catch (error) { console.log(`Calculation validation test skipped: ${error.message}`); } }); // Test validation caching tap.test('Validation Suite - Validation result caching', async () => { const xmlBuffer = await TestFileHelpers.loadTestFile( `${TestFileCategories.UBL_XMLRECHNUNG}/EN16931_Einfach.ubl.xml` ); const einvoice = await EInvoice.fromXml(xmlBuffer.toString('utf-8')); // First validation (cold) const { duration: coldDuration } = await PerformanceUtils.measure( 'validation-cold', async () => einvoice.validate(ValidationLevel.BUSINESS) ); // Second validation (potentially cached) const { duration: warmDuration } = await PerformanceUtils.measure( 'validation-warm', async () => einvoice.validate(ValidationLevel.BUSINESS) ); console.log(`Cold validation: ${coldDuration.toFixed(2)}ms`); console.log(`Warm validation: ${warmDuration.toFixed(2)}ms`); // Check if errors are consistent const errors1 = einvoice.getValidationErrors(); const errors2 = einvoice.getValidationErrors(); expect(errors1).toEqual(errors2); }); // Generate validation summary tap.test('Validation Suite - Summary Report', async () => { const stats = PerformanceUtils.getStats('br-validation'); if (stats) { console.log('\nBusiness Rule Validation Performance:'); console.log(` Total validations: ${stats.count}`); console.log(` Average time: ${stats.avg.toFixed(2)}ms`); console.log(` Min/Max: ${stats.min.toFixed(2)}ms / ${stats.max.toFixed(2)}ms`); } // Test that validation is generally performant if (stats && stats.count > 10) { expect(stats.avg).toBeLessThan(100); // Should validate in under 100ms on average } }); tap.start();