import { tap, expect } from '@git.zone/tstest/tapbundle'; import { EInvoice } from '../../../ts/index.js'; import { ValidationLevel } from '../../../ts/interfaces/common.js'; import { CorpusLoader, PerformanceTracker } from '../../helpers/test-utils.js'; import * as path from 'path'; /** * Test ID: CORP-06 * Test Description: EN16931 Test Suite Execution * Priority: High * * This test executes the official EN16931 validation test suite * to ensure compliance with the European e-invoicing standard. */ tap.test('CORP-06: EN16931 Test Suite Execution - should validate against EN16931 test cases', async (t) => { // Load EN16931 test files const en16931Files = await CorpusLoader.loadCategory('EN16931_TEST_CASES'); console.log(`Testing ${en16931Files.length} EN16931 test cases`); const results = { total: en16931Files.length, passed: 0, failed: 0, ruleCategories: new Map(), processingTimes: [] as number[], businessRules: { passed: 0, failed: 0 }, codelistRules: { passed: 0, failed: 0 }, calculationRules: { passed: 0, failed: 0 }, syntaxRules: { passed: 0, failed: 0 } }; const failures: Array<{ file: string; rule: string; expected: 'pass' | 'fail'; actual: 'pass' | 'fail'; error?: string; }> = []; for (const file of en16931Files) { const filename = path.basename(file.path); // Determine expected result and rule from filename // EN16931 test files typically follow pattern: BR-XX.xml, BR-CL-XX.xml, BR-CO-XX.xml const ruleMatch = filename.match(/^(BR|BR-CL|BR-CO|BR-[A-Z]+)-(\d+)/); const rule = ruleMatch ? ruleMatch[0] : 'unknown'; const ruleCategory = ruleMatch ? ruleMatch[1] : 'unknown'; // Some test files are designed to fail validation const shouldFail = filename.includes('fail') || filename.includes('invalid'); try { const xmlBuffer = await CorpusLoader.loadFile(file.path); const xmlString = xmlBuffer.toString('utf-8'); // Track performance const { result: invoice, metric } = await PerformanceTracker.track( 'en16931-validation', async () => { const einvoice = new EInvoice(); await einvoice.fromXmlString(xmlString); return einvoice; }, { file: file.path, rule, size: file.size } ); results.processingTimes.push(metric.duration); // Validate against EN16931 rules const validationResult = await invoice.validate(ValidationLevel.EN16931); // Track rule category if (!results.ruleCategories.has(ruleCategory)) { results.ruleCategories.set(ruleCategory, { passed: 0, failed: 0 }); } // Categorize rules if (ruleCategory === 'BR-CL') { if (validationResult.valid) results.codelistRules.passed++; else results.codelistRules.failed++; } else if (ruleCategory === 'BR-CO') { if (validationResult.valid) results.calculationRules.passed++; else results.calculationRules.failed++; } else if (ruleCategory === 'BR') { if (validationResult.valid) results.businessRules.passed++; else results.businessRules.failed++; } else { if (validationResult.valid) results.syntaxRules.passed++; else results.syntaxRules.failed++; } // Check if result matches expectation const actuallyFailed = !validationResult.valid; if (shouldFail === actuallyFailed) { results.passed++; const category = results.ruleCategories.get(ruleCategory)!; category.passed++; t.pass(`✓ ${filename} [${rule}]: ${shouldFail ? 'Failed as expected' : 'Passed as expected'}`); if (actuallyFailed && validationResult.errors?.length) { t.pass(` - Error: ${validationResult.errors[0].message}`); } } else { results.failed++; const category = results.ruleCategories.get(ruleCategory)!; category.failed++; failures.push({ file: filename, rule, expected: shouldFail ? 'fail' : 'pass', actual: actuallyFailed ? 'fail' : 'pass', error: validationResult.errors?.[0]?.message }); t.fail(`✗ ${filename} [${rule}]: Expected to ${shouldFail ? 'fail' : 'pass'} but ${actuallyFailed ? 'failed' : 'passed'}`); } } catch (error: any) { // Parse errors might be expected for some test cases if (shouldFail) { results.passed++; t.pass(`✓ ${filename} [${rule}]: Failed to parse as expected`); } else { results.failed++; failures.push({ file: filename, rule, expected: 'pass', actual: 'fail', error: error.message }); t.fail(`✗ ${filename} [${rule}]: Unexpected parse error`); } } } // Summary report console.log('\n=== EN16931 Test Suite Execution Summary ==='); console.log(`Total test cases: ${results.total}`); console.log(`Passed: ${results.passed} (${(results.passed/results.total*100).toFixed(1)}%)`); console.log(`Failed: ${results.failed}`); console.log('\nRule Categories:'); results.ruleCategories.forEach((stats, category) => { const total = stats.passed + stats.failed; console.log(` ${category}: ${stats.passed}/${total} passed (${(stats.passed/total*100).toFixed(1)}%)`); }); console.log('\nRule Types:'); console.log(` Business Rules (BR): ${results.businessRules.passed}/${results.businessRules.passed + results.businessRules.failed} passed`); console.log(` Codelist Rules (BR-CL): ${results.codelistRules.passed}/${results.codelistRules.passed + results.codelistRules.failed} passed`); console.log(` Calculation Rules (BR-CO): ${results.calculationRules.passed}/${results.calculationRules.passed + results.calculationRules.failed} passed`); console.log(` Syntax Rules: ${results.syntaxRules.passed}/${results.syntaxRules.passed + results.syntaxRules.failed} passed`); if (failures.length > 0) { console.log('\nFailure Details (first 10):'); failures.slice(0, 10).forEach(f => { console.log(` ${f.file} [${f.rule}]:`); console.log(` Expected: ${f.expected}, Actual: ${f.actual}`); if (f.error) console.log(` Error: ${f.error}`); }); if (failures.length > 10) { console.log(` ... and ${failures.length - 10} more failures`); } } // Performance metrics if (results.processingTimes.length > 0) { const avgTime = results.processingTimes.reduce((a, b) => a + b, 0) / results.processingTimes.length; console.log('\nPerformance Metrics:'); console.log(` Average validation time: ${avgTime.toFixed(2)}ms`); console.log(` Total execution time: ${results.processingTimes.reduce((a, b) => a + b, 0).toFixed(0)}ms`); } // Success criteria: at least 95% of test cases should behave as expected const successRate = results.passed / results.total; expect(successRate).toBeGreaterThan(0.95); }); tap.start();