325 lines
12 KiB
TypeScript
325 lines
12 KiB
TypeScript
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
|
import { promises as fs } from 'fs';
|
||
|
import * as path from 'path';
|
||
|
import { CorpusLoader } from '../../helpers/corpus.loader.js';
|
||
|
import { PerformanceTracker } from '../../helpers/performance.tracker.js';
|
||
|
|
||
|
tap.test('VAL-04: XSD Schema Validation - should validate against XML Schema definitions', async () => {
|
||
|
// Test schema validation for different formats
|
||
|
const schemaTests = [
|
||
|
{
|
||
|
category: 'UBL_XMLRECHNUNG',
|
||
|
schemaType: 'UBL 2.1',
|
||
|
description: 'UBL invoices should validate against UBL 2.1 schema'
|
||
|
},
|
||
|
{
|
||
|
category: 'CII_XMLRECHNUNG',
|
||
|
schemaType: 'UN/CEFACT CII',
|
||
|
description: 'CII invoices should validate against UN/CEFACT schema'
|
||
|
},
|
||
|
{
|
||
|
category: 'EN16931_UBL_EXAMPLES',
|
||
|
schemaType: 'UBL 2.1',
|
||
|
description: 'EN16931 UBL examples should be schema-valid'
|
||
|
}
|
||
|
] as const;
|
||
|
|
||
|
console.log('Testing XSD schema validation across formats');
|
||
|
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
let totalFiles = 0;
|
||
|
let validFiles = 0;
|
||
|
let invalidFiles = 0;
|
||
|
let errorFiles = 0;
|
||
|
|
||
|
for (const test of schemaTests) {
|
||
|
try {
|
||
|
const files = await CorpusLoader.getFiles(test.category);
|
||
|
const xmlFiles = files.filter(f => f.endsWith('.xml')).slice(0, 3); // Test 3 per category
|
||
|
|
||
|
if (xmlFiles.length === 0) {
|
||
|
console.log(`\n${test.category}: No XML files found, skipping`);
|
||
|
continue;
|
||
|
}
|
||
|
|
||
|
console.log(`\n${test.category} (${test.schemaType}): Testing ${xmlFiles.length} files`);
|
||
|
|
||
|
for (const filePath of xmlFiles) {
|
||
|
const fileName = path.basename(filePath);
|
||
|
totalFiles++;
|
||
|
|
||
|
try {
|
||
|
const xmlContent = await fs.readFile(filePath, 'utf-8');
|
||
|
|
||
|
const { result: einvoice } = await PerformanceTracker.track(
|
||
|
'schema-xml-loading',
|
||
|
async () => await EInvoice.fromXml(xmlContent)
|
||
|
);
|
||
|
|
||
|
// Perform schema validation (if available)
|
||
|
const { result: validation } = await PerformanceTracker.track(
|
||
|
'xsd-schema-validation',
|
||
|
async () => {
|
||
|
// Try to validate with schema validation level
|
||
|
return await einvoice.validate(/* ValidationLevel.SCHEMA */);
|
||
|
},
|
||
|
{
|
||
|
category: test.category,
|
||
|
file: fileName,
|
||
|
schemaType: test.schemaType
|
||
|
}
|
||
|
);
|
||
|
|
||
|
if (validation.valid) {
|
||
|
validFiles++;
|
||
|
console.log(` ✓ ${fileName}: Schema valid`);
|
||
|
} else {
|
||
|
invalidFiles++;
|
||
|
console.log(` ○ ${fileName}: Schema validation failed`);
|
||
|
if (validation.errors && validation.errors.length > 0) {
|
||
|
const schemaErrors = validation.errors.filter(e =>
|
||
|
e.message && (
|
||
|
e.message.toLowerCase().includes('schema') ||
|
||
|
e.message.toLowerCase().includes('xsd') ||
|
||
|
e.message.toLowerCase().includes('element')
|
||
|
)
|
||
|
);
|
||
|
console.log(` Schema errors: ${schemaErrors.length}`);
|
||
|
schemaErrors.slice(0, 2).forEach(err => {
|
||
|
console.log(` - ${err.code}: ${err.message}`);
|
||
|
});
|
||
|
}
|
||
|
}
|
||
|
|
||
|
} catch (error) {
|
||
|
errorFiles++;
|
||
|
console.log(` ✗ ${fileName}: Error - ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
} catch (error) {
|
||
|
console.log(`Error testing ${test.category}: ${error.message}`);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
console.log('\n=== XSD SCHEMA VALIDATION SUMMARY ===');
|
||
|
console.log(`Total files tested: ${totalFiles}`);
|
||
|
console.log(`Schema valid: ${validFiles}`);
|
||
|
console.log(`Schema invalid: ${invalidFiles}`);
|
||
|
console.log(`Errors: ${errorFiles}`);
|
||
|
|
||
|
if (totalFiles > 0) {
|
||
|
const validationRate = (validFiles / totalFiles * 100).toFixed(1);
|
||
|
console.log(`Validation rate: ${validationRate}%`);
|
||
|
|
||
|
// Performance summary
|
||
|
const perfSummary = await PerformanceTracker.getSummary('xsd-schema-validation');
|
||
|
if (perfSummary) {
|
||
|
console.log(`\nSchema Validation Performance:`);
|
||
|
console.log(` Average: ${perfSummary.average.toFixed(2)}ms`);
|
||
|
console.log(` Min: ${perfSummary.min.toFixed(2)}ms`);
|
||
|
console.log(` Max: ${perfSummary.max.toFixed(2)}ms`);
|
||
|
console.log(` P95: ${perfSummary.p95.toFixed(2)}ms`);
|
||
|
}
|
||
|
|
||
|
// Expect most files to process successfully (valid or invalid, but not error)
|
||
|
expect((validFiles + invalidFiles) / totalFiles).toBeGreaterThan(0.8);
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('VAL-04: Schema Validation Error Types - should identify different types of schema violations', async () => {
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
|
||
|
const schemaViolationTests = [
|
||
|
{
|
||
|
name: 'Missing required element',
|
||
|
xml: `<?xml version="1.0"?>
|
||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||
|
<!-- Missing required ID element -->
|
||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
</Invoice>`,
|
||
|
violationType: 'missing-element'
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid element order',
|
||
|
xml: `<?xml version="1.0"?>
|
||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
<cbc:ID>WRONG-ORDER</cbc:ID> <!-- ID should come before IssueDate -->
|
||
|
</Invoice>`,
|
||
|
violationType: 'element-order'
|
||
|
},
|
||
|
{
|
||
|
name: 'Invalid data type',
|
||
|
xml: `<?xml version="1.0"?>
|
||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||
|
<cbc:ID>VALID-ID</cbc:ID>
|
||
|
<cbc:IssueDate>not-a-date</cbc:IssueDate> <!-- Invalid date format -->
|
||
|
</Invoice>`,
|
||
|
violationType: 'data-type'
|
||
|
},
|
||
|
{
|
||
|
name: 'Unexpected element',
|
||
|
xml: `<?xml version="1.0"?>
|
||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||
|
<cbc:ID>VALID-ID</cbc:ID>
|
||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
<UnknownElement>Not allowed</UnknownElement> <!-- Not in schema -->
|
||
|
</Invoice>`,
|
||
|
violationType: 'unexpected-element'
|
||
|
}
|
||
|
];
|
||
|
|
||
|
for (const test of schemaViolationTests) {
|
||
|
try {
|
||
|
const { result: validation } = await PerformanceTracker.track(
|
||
|
'schema-violation-test',
|
||
|
async () => {
|
||
|
const einvoice = await EInvoice.fromXml(test.xml);
|
||
|
return await einvoice.validate();
|
||
|
}
|
||
|
);
|
||
|
|
||
|
console.log(`${test.name}: ${validation.valid ? 'VALID' : 'INVALID'}`);
|
||
|
|
||
|
if (!validation.valid && validation.errors) {
|
||
|
const schemaErrors = validation.errors.filter(e =>
|
||
|
e.message && (
|
||
|
e.message.toLowerCase().includes('schema') ||
|
||
|
e.message.toLowerCase().includes('element') ||
|
||
|
e.message.toLowerCase().includes('type')
|
||
|
)
|
||
|
);
|
||
|
|
||
|
console.log(` Schema errors detected: ${schemaErrors.length}`);
|
||
|
schemaErrors.slice(0, 1).forEach(err => {
|
||
|
console.log(` - ${err.code}: ${err.message}`);
|
||
|
});
|
||
|
|
||
|
// Should detect schema violations
|
||
|
expect(schemaErrors.length).toBeGreaterThan(0);
|
||
|
} else {
|
||
|
console.log(` ○ No schema violations detected (may need stricter validation)`);
|
||
|
}
|
||
|
|
||
|
} catch (error) {
|
||
|
console.log(`${test.name}: Error - ${error.message}`);
|
||
|
// Parsing errors are also a form of schema violation
|
||
|
console.log(` ✓ Error during parsing indicates schema violation`);
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('VAL-04: Schema Validation Performance - should validate schemas efficiently', async () => {
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
|
||
|
// Generate test XMLs of different sizes
|
||
|
function generateUBLInvoice(lineItems: number): string {
|
||
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||
|
<cbc:ID>PERF-${Date.now()}</cbc:ID>
|
||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>`;
|
||
|
|
||
|
for (let i = 1; i <= lineItems; i++) {
|
||
|
xml += `
|
||
|
<cac:InvoiceLine>
|
||
|
<cbc:ID>${i}</cbc:ID>
|
||
|
<cbc:InvoicedQuantity unitCode="EA">${i}</cbc:InvoicedQuantity>
|
||
|
<cbc:LineExtensionAmount currencyID="EUR">${i * 100}</cbc:LineExtensionAmount>
|
||
|
</cac:InvoiceLine>`;
|
||
|
}
|
||
|
|
||
|
xml += '\n</Invoice>';
|
||
|
return xml;
|
||
|
}
|
||
|
|
||
|
const performanceTests = [
|
||
|
{ name: 'Small invoice (5 lines)', lineItems: 5, threshold: 50 },
|
||
|
{ name: 'Medium invoice (25 lines)', lineItems: 25, threshold: 100 },
|
||
|
{ name: 'Large invoice (100 lines)', lineItems: 100, threshold: 200 }
|
||
|
];
|
||
|
|
||
|
console.log('Testing schema validation performance');
|
||
|
|
||
|
for (const test of performanceTests) {
|
||
|
const xml = generateUBLInvoice(test.lineItems);
|
||
|
console.log(`\n${test.name} (${Math.round(xml.length/1024)}KB)`);
|
||
|
|
||
|
const { metric } = await PerformanceTracker.track(
|
||
|
'schema-performance-test',
|
||
|
async () => {
|
||
|
const einvoice = await EInvoice.fromXml(xml);
|
||
|
return await einvoice.validate();
|
||
|
}
|
||
|
);
|
||
|
|
||
|
console.log(` Validation time: ${metric.duration.toFixed(2)}ms`);
|
||
|
console.log(` Memory used: ${metric.memory ? (metric.memory.used / 1024 / 1024).toFixed(2) : 'N/A'}MB`);
|
||
|
|
||
|
// Performance assertions
|
||
|
expect(metric.duration).toBeLessThan(test.threshold);
|
||
|
|
||
|
if (metric.memory && metric.memory.used > 0) {
|
||
|
const memoryMB = metric.memory.used / 1024 / 1024;
|
||
|
expect(memoryMB).toBeLessThan(100); // Should not use more than 100MB
|
||
|
}
|
||
|
}
|
||
|
});
|
||
|
|
||
|
tap.test('VAL-04: Schema Validation Caching - should cache schema validation results', async () => {
|
||
|
const { EInvoice } = await import('../../../ts/index.js');
|
||
|
|
||
|
const testXml = `<?xml version="1.0"?>
|
||
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||
|
<cbc:ID>CACHE-TEST</cbc:ID>
|
||
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
||
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||
|
</Invoice>`;
|
||
|
|
||
|
console.log('Testing schema validation caching');
|
||
|
|
||
|
const einvoice = await EInvoice.fromXml(testXml);
|
||
|
|
||
|
// First validation (cold)
|
||
|
const { metric: coldMetric } = await PerformanceTracker.track(
|
||
|
'schema-validation-cold',
|
||
|
async () => await einvoice.validate()
|
||
|
);
|
||
|
|
||
|
// Second validation (potentially cached)
|
||
|
const { metric: warmMetric } = await PerformanceTracker.track(
|
||
|
'schema-validation-warm',
|
||
|
async () => await einvoice.validate()
|
||
|
);
|
||
|
|
||
|
console.log(`Cold validation: ${coldMetric.duration.toFixed(2)}ms`);
|
||
|
console.log(`Warm validation: ${warmMetric.duration.toFixed(2)}ms`);
|
||
|
|
||
|
// Warm validation should not be significantly slower
|
||
|
const speedupRatio = coldMetric.duration / warmMetric.duration;
|
||
|
console.log(`Speedup ratio: ${speedupRatio.toFixed(2)}x`);
|
||
|
|
||
|
// Either caching helps (speedup) or both are fast
|
||
|
const bothFast = coldMetric.duration < 20 && warmMetric.duration < 20;
|
||
|
const cachingHelps = speedupRatio > 1.2;
|
||
|
|
||
|
if (cachingHelps) {
|
||
|
console.log('✓ Caching appears to improve performance');
|
||
|
} else if (bothFast) {
|
||
|
console.log('✓ Both validations are fast (caching may not be needed)');
|
||
|
} else {
|
||
|
console.log('○ Caching behavior unclear');
|
||
|
}
|
||
|
|
||
|
expect(bothFast || cachingHelps).toEqual(true);
|
||
|
});
|
||
|
|
||
|
tap.start();
|