- 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.
421 lines
15 KiB
TypeScript
421 lines
15 KiB
TypeScript
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('<testSet')) {
|
|
// Extract the Invoice element from within the test wrapper
|
|
const invoiceMatch = xmlString.match(/<Invoice[^>]*>[\s\S]*?<\/Invoice>/);
|
|
if (invoiceMatch) {
|
|
// Add proper namespaces to make it a valid UBL invoice
|
|
xmlString = `<?xml version="1.0" encoding="UTF-8"?>
|
|
${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('<testSet')) {
|
|
const invoiceMatch = xmlString.match(/<Invoice[^>]*>[\s\S]*?<\/Invoice>/);
|
|
if (invoiceMatch) {
|
|
xmlString = `<?xml version="1.0" encoding="UTF-8"?>
|
|
${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 = `<?xml version="1.0"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<ID>123</ID>
|
|
<IssueDate>not-a-date</IssueDate>
|
|
<InvalidElement>This element doesn't belong here</InvalidElement>
|
|
</Invoice>`;
|
|
|
|
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(); |