185 lines
7.0 KiB
TypeScript
185 lines
7.0 KiB
TypeScript
|
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<string, { passed: number; failed: number }>(),
|
||
|
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();
|