einvoice/test/test.conversion.ts
Philipp Kunz 56fd12a6b2 test(suite): comprehensive test suite improvements and new validators
- 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.
2025-05-30 18:18:42 +00:00

409 lines
14 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { EInvoice, EInvoiceFormatError } from '../ts/index.js';
import { InvoiceFormat } from '../ts/interfaces/common.js';
import { TestFileHelpers, TestFileCategories, PerformanceUtils, TestInvoiceFactory } from './helpers/utils.js';
import * as path from 'path';
/**
* Cross-format conversion test suite
*/
// Test conversion between CII and UBL using paired files
tap.test('Conversion - CII to UBL using XML-Rechnung pairs', async () => {
// Get matching CII and UBL files
const ciiFiles = await TestFileHelpers.getTestFiles(TestFileCategories.CII_XMLRECHNUNG, '*.xml');
const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
// Find paired files (same base name)
const pairs: Array<{cii: string, ubl: string, name: string}> = [];
for (const ciiFile of ciiFiles) {
const baseName = path.basename(ciiFile).replace('.cii.xml', '');
const matchingUbl = ublFiles.find(ubl =>
path.basename(ubl).startsWith(baseName) && ubl.endsWith('.ubl.xml')
);
if (matchingUbl) {
pairs.push({ cii: ciiFile, ubl: matchingUbl, name: baseName });
}
}
console.log(`Found ${pairs.length} CII/UBL pairs for conversion testing`);
let successCount = 0;
const conversionIssues: string[] = [];
for (const pair of pairs.slice(0, 5)) { // Test first 5 pairs
try {
// Load CII invoice
const ciiBuffer = await TestFileHelpers.loadTestFile(pair.cii);
const ciiInvoice = await EInvoice.fromXml(ciiBuffer.toString('utf-8'));
// Convert to UBL
const { result: ublXml, duration } = await PerformanceUtils.measure(
'cii-to-ubl',
async () => ciiInvoice.exportXml('ubl')
);
expect(ublXml).toBeTruthy();
expect(ublXml).toInclude('xmlns:cbc=');
expect(ublXml).toInclude('xmlns:cac=');
// Load the converted UBL back
const convertedInvoice = await EInvoice.fromXml(ublXml);
// Verify key fields are preserved
verifyFieldMapping(ciiInvoice, convertedInvoice, pair.name);
successCount++;
console.log(`${pair.name}: CII→UBL conversion successful (${duration.toFixed(2)}ms)`);
} catch (error) {
const issue = `${pair.name}: ${error.message}`;
conversionIssues.push(issue);
console.log(`${issue}`);
}
}
console.log(`\nConversion Summary: ${successCount}/${pairs.length} successful`);
if (conversionIssues.length > 0) {
console.log('Issues:', conversionIssues);
}
});
// Test conversion from UBL to CII
tap.test('Conversion - UBL to CII reverse conversion', async () => {
const ublFiles = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
console.log(`Testing UBL to CII conversion with ${ublFiles.length} files`);
for (const file of ublFiles.slice(0, 3)) {
const fileName = path.basename(file);
try {
const ublBuffer = await TestFileHelpers.loadTestFile(file);
const ublInvoice = await EInvoice.fromXml(ublBuffer.toString('utf-8'));
// Skip if detected as XRechnung (might have special requirements)
if (ublInvoice.getFormat() === InvoiceFormat.XRECHNUNG) {
console.log(`${fileName}: Skipping XRechnung-specific file`);
continue;
}
// Convert to CII (Factur-X)
const ciiXml = await ublInvoice.exportXml('facturx');
expect(ciiXml).toBeTruthy();
expect(ciiXml).toInclude('CrossIndustryInvoice');
expect(ciiXml).toInclude('ExchangedDocument');
// Verify round-trip
const ciiInvoice = await EInvoice.fromXml(ciiXml);
expect(ciiInvoice.invoiceId).toEqual(ublInvoice.invoiceId);
console.log(`${fileName}: UBL→CII conversion successful`);
} catch (error) {
if (error instanceof EInvoiceFormatError) {
console.log(`${fileName}: Format error - ${error.message}`);
if (error.unsupportedFeatures) {
console.log(` Unsupported features: ${error.unsupportedFeatures.join(', ')}`);
}
} else {
console.log(`${fileName}: ${error.message}`);
}
}
}
});
// Test ZUGFeRD to XRechnung conversion
tap.test('Conversion - ZUGFeRD to XRechnung format', async () => {
const zugferdPdfs = await TestFileHelpers.getTestFiles(TestFileCategories.ZUGFERD_V2_CORRECT, '*.pdf');
let tested = 0;
for (const file of zugferdPdfs.slice(0, 3)) {
const fileName = path.basename(file);
try {
// Extract from PDF
const pdfBuffer = await TestFileHelpers.loadTestFile(file);
const zugferdInvoice = await EInvoice.fromPdf(pdfBuffer);
// Convert to XRechnung
const xrechnungXml = await zugferdInvoice.exportXml('xrechnung');
expect(xrechnungXml).toBeTruthy();
// XRechnung should be UBL format with specific extensions
if (xrechnungXml.includes('Invoice xmlns')) {
expect(xrechnungXml).toInclude('CustomizationID');
expect(xrechnungXml).toInclude('urn:cen.eu:en16931');
}
tested++;
console.log(`${fileName}: ZUGFeRD→XRechnung conversion successful`);
} catch (error) {
console.log(`${fileName}: Conversion not available - ${error.message}`);
}
}
if (tested === 0) {
console.log('Note: ZUGFeRD to XRechnung conversion may need implementation');
}
});
// Test data loss detection during conversion
tap.test('Conversion - Data loss detection and reporting', async () => {
// Create a complex invoice with all possible fields
const complexInvoice = new EInvoice();
Object.assign(complexInvoice, TestInvoiceFactory.createComplexInvoice());
// Add format-specific fields
complexInvoice.buyerReference = 'PO-2024-12345';
complexInvoice.electronicAddress = {
scheme: '0088',
value: '1234567890123'
};
complexInvoice.notes = [
'Special handling required',
'Express delivery requested',
'Contact buyer before delivery'
];
// Generate source XML
const sourceXml = await complexInvoice.exportXml('facturx');
await complexInvoice.loadXml(sourceXml);
// Test conversions and check for data loss
const formats: Array<{from: string, to: string}> = [
{ from: 'facturx', to: 'ubl' },
{ from: 'facturx', to: 'xrechnung' },
{ from: 'facturx', to: 'zugferd' }
];
for (const conversion of formats) {
console.log(`\nTesting ${conversion.from}${conversion.to} conversion:`);
try {
const convertedXml = await complexInvoice.exportXml(conversion.to as any);
const convertedInvoice = await EInvoice.fromXml(convertedXml);
// Check for data preservation
const issues = checkDataPreservation(complexInvoice, convertedInvoice);
if (issues.length === 0) {
console.log(`✓ All data preserved in ${conversion.to} format`);
} else {
console.log(`⚠ Data loss detected in ${conversion.to} format:`);
issues.forEach(issue => console.log(` - ${issue}`));
}
} catch (error) {
console.log(`✗ Conversion failed: ${error.message}`);
}
}
});
// Test edge cases in conversion
tap.test('Conversion - Edge cases and special characters', async () => {
const edgeCaseInvoice = new EInvoice();
Object.assign(edgeCaseInvoice, TestInvoiceFactory.createMinimalInvoice());
// Add edge case data
edgeCaseInvoice.from.name = 'Müller & Söhne GmbH & Co. KG';
edgeCaseInvoice.to.name = 'L\'Entreprise Française S.à.r.l.';
edgeCaseInvoice.items[0].name = 'Product with "quotes" and <tags>';
edgeCaseInvoice.notes = ['Note with € symbol', 'Japanese: こんにちは'];
// Test conversion with special characters
const formats = ['facturx', 'ubl', 'xrechnung'] as const;
for (const format of formats) {
try {
const xml = await edgeCaseInvoice.exportXml(format);
// Verify special characters are properly encoded
expect(xml).toInclude('Müller');
expect(xml).toInclude('Française');
expect(xml).toContain('&quot;'); // Encoded quotes
expect(xml).toContain('&lt;'); // Encoded less-than
// Verify it can be parsed back
const parsed = await EInvoice.fromXml(xml);
expect(parsed.from.name).toEqual('Müller & Söhne GmbH & Co. KG');
console.log(`${format}: Special characters handled correctly`);
} catch (error) {
console.log(`${format}: Failed with special characters - ${error.message}`);
}
}
});
// Test batch conversion performance
tap.test('Conversion - Batch conversion performance', async () => {
const files = await TestFileHelpers.getTestFiles(TestFileCategories.UBL_XMLRECHNUNG, '*.xml');
const batchSize = Math.min(10, files.length);
console.log(`Testing batch conversion of ${batchSize} files`);
const startTime = performance.now();
const results = await Promise.all(
files.slice(0, batchSize).map(async (file) => {
try {
const buffer = await TestFileHelpers.loadTestFile(file);
const invoice = await EInvoice.fromXml(buffer.toString('utf-8'));
const converted = await invoice.exportXml('facturx');
return { success: true, size: converted.length };
} catch (error) {
return { success: false, error: error.message };
}
})
);
const duration = performance.now() - startTime;
const successCount = results.filter(r => r.success).length;
console.log(`✓ Batch conversion completed in ${duration.toFixed(2)}ms`);
console.log(` Success rate: ${successCount}/${batchSize}`);
console.log(` Average time per conversion: ${(duration / batchSize).toFixed(2)}ms`);
expect(duration / batchSize).toBeLessThan(500); // Should be under 500ms per conversion
});
// Test format-specific extensions preservation
tap.test('Conversion - Format-specific extensions', async () => {
// This tests that format-specific extensions don't break conversion
const extensionTests = [
{
name: 'XRechnung BuyerReference',
xml: `<?xml version="1.0"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<CustomizationID>urn:cen.eu:en16931:2017#compliant#urn:xrechnung:cius:2.0</CustomizationID>
<ID>123</ID>
<BuyerReference>04011000-12345-03</BuyerReference>
</Invoice>`
}
];
for (const test of extensionTests) {
try {
const invoice = await EInvoice.fromXml(test.xml);
// Convert to CII
const ciiXml = await invoice.exportXml('facturx');
expect(ciiXml).toBeTruthy();
// Convert back to UBL
const ciiInvoice = await EInvoice.fromXml(ciiXml);
const ublXml = await ciiInvoice.exportXml('ubl');
// Check if buyer reference survived
if (invoice.buyerReference) {
expect(ublXml).toInclude(invoice.buyerReference);
}
console.log(`${test.name}: Extension preserved through conversion`);
} catch (error) {
console.log(`${test.name}: ${error.message}`);
}
}
});
// Test conversion error handling
tap.test('Conversion - Error handling and recovery', async () => {
// Test with minimal invalid invoice
const invalidInvoice = new EInvoice();
invalidInvoice.id = 'TEST-INVALID';
// Missing required fields like from, to, items
try {
await invalidInvoice.exportXml('facturx');
throw new Error('Should have thrown an error for invalid invoice');
} catch (error) {
console.log(`✓ Invalid invoice error caught: ${error.message}`);
if (error instanceof EInvoiceFormatError) {
console.log(` Compatibility report:\n${error.getCompatibilityReport()}`);
}
}
});
// Performance summary for conversions
tap.test('Conversion - Performance Summary', async () => {
const conversionStats = PerformanceUtils.getStats('cii-to-ubl');
if (conversionStats) {
console.log('\nConversion Performance:');
console.log(`CII to UBL conversions: ${conversionStats.count}`);
console.log(`Average time: ${conversionStats.avg.toFixed(2)}ms`);
console.log(`Min/Max: ${conversionStats.min.toFixed(2)}ms / ${conversionStats.max.toFixed(2)}ms`);
// Conversions should be reasonably fast
expect(conversionStats.avg).toBeLessThan(100);
}
});
// Helper function to verify field mapping
function verifyFieldMapping(source: EInvoice, converted: EInvoice, testName: string): void {
const criticalFields = [
{ field: 'invoiceId', name: 'Invoice ID' },
{ field: 'date', name: 'Invoice Date' },
{ field: 'currency', name: 'Currency' }
];
for (const check of criticalFields) {
const sourceVal = source[check.field as keyof EInvoice];
const convertedVal = converted[check.field as keyof EInvoice];
if (sourceVal !== convertedVal) {
console.log(`${check.name} mismatch: ${sourceVal}${convertedVal}`);
}
}
// Check seller/buyer
if (source.from.name !== converted.from.name) {
console.log(` ⚠ Seller name mismatch: ${source.from.name}${converted.from.name}`);
}
if (source.to.name !== converted.to.name) {
console.log(` ⚠ Buyer name mismatch: ${source.to.name}${converted.to.name}`);
}
// Check items count
if (source.items.length !== converted.items.length) {
console.log(` ⚠ Items count mismatch: ${source.items.length}${converted.items.length}`);
}
}
// Helper function to check data preservation
function checkDataPreservation(source: EInvoice, converted: EInvoice): string[] {
const issues: string[] = [];
// Check basic fields
if (source.invoiceId !== converted.invoiceId) {
issues.push(`Invoice ID changed: ${source.invoiceId}${converted.invoiceId}`);
}
if (source.buyerReference && source.buyerReference !== converted.buyerReference) {
issues.push(`Buyer reference lost or changed`);
}
if (source.notes && source.notes.length !== converted.notes?.length) {
issues.push(`Notes count changed: ${source.notes.length}${converted.notes?.length || 0}`);
}
if (source.electronicAddress && !converted.electronicAddress) {
issues.push(`Electronic address lost`);
}
// Check payment details
if (source.paymentOptions?.sepaConnection?.iban !== converted.paymentOptions?.sepaConnection?.iban) {
issues.push(`IBAN changed or lost`);
}
return issues;
}
tap.start();