389 lines
13 KiB
TypeScript
389 lines
13 KiB
TypeScript
import { tap, expect } from '@push.rocks/tapbundle';
|
|
import { EInvoice, EInvoiceValidationError } from '../ts/index.js';
|
|
import { ValidationLevel, InvoiceFormat } from '../ts/interfaces/common.js';
|
|
import { TestFileHelpers, TestFileCategories, InvoiceAssertions, PerformanceUtils } from './test-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);
|
|
const xmlString = xmlBuffer.toString('utf-8');
|
|
|
|
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);
|
|
const xmlString = xmlBuffer.toString('utf-8');
|
|
|
|
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) {
|
|
expect(error.validationErrors).toHaveLength(1);
|
|
expect(error.validationErrors[0].code).toEqual('VAL-001');
|
|
console.log('✓ Empty invoice validation error handled correctly');
|
|
}
|
|
}
|
|
|
|
// 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',
|
|
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',
|
|
unitQuantity: 5,
|
|
unitNetPrice: 100, // Total: 500
|
|
vatPercentage: 19 // VAT: 95
|
|
},
|
|
{
|
|
position: 2,
|
|
name: 'Product B',
|
|
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(); |