2025-05-25 19:45:37 +00:00
|
|
|
|
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
|
|
|
import * as einvoice from '../../../ts/index.js';
|
|
|
|
|
import * as plugins from '../../plugins.js';
|
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
tap.test('PARSE-05: Namespace Resolution - Basic namespace declarations', async () => {
|
|
|
|
|
console.log('Testing namespace resolution in e-invoices...\n');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
const namespaceTests = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Default namespace',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
|
|
|
<ID>TEST-001</ID>
|
|
|
|
|
<IssueDate>2024-01-01</IssueDate>
|
|
|
|
|
</Invoice>`,
|
2025-05-28 18:46:18 +00:00
|
|
|
|
expectedNamespaces: [{
|
|
|
|
|
prefix: '',
|
|
|
|
|
uri: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
|
|
|
|
|
}]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Prefixed namespace',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
<ubl:Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
|
|
|
<ubl:ID>TEST-002</ubl:ID>
|
|
|
|
|
<ubl:IssueDate>2024-01-01</ubl:IssueDate>
|
|
|
|
|
</ubl:Invoice>`,
|
2025-05-28 18:46:18 +00:00
|
|
|
|
expectedNamespaces: [{
|
|
|
|
|
prefix: 'ubl',
|
|
|
|
|
uri: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2'
|
|
|
|
|
}]
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Multiple namespaces',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
<ubl:Invoice
|
|
|
|
|
xmlns:ubl="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>TEST-003</cbc:ID>
|
|
|
|
|
<cac:AccountingSupplierParty>
|
|
|
|
|
<cac:Party>
|
|
|
|
|
<cbc:Name>Test Supplier</cbc:Name>
|
|
|
|
|
</cac:Party>
|
|
|
|
|
</cac:AccountingSupplierParty>
|
|
|
|
|
</ubl:Invoice>`,
|
2025-05-28 18:46:18 +00:00
|
|
|
|
expectedNamespaces: [
|
|
|
|
|
{ prefix: 'ubl', uri: 'urn:oasis:names:specification:ubl:schema:xsd:Invoice-2' },
|
|
|
|
|
{ prefix: 'cac', uri: 'urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2' },
|
|
|
|
|
{ prefix: 'cbc', uri: 'urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2' }
|
|
|
|
|
]
|
|
|
|
|
}
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
for (const test of namespaceTests) {
|
|
|
|
|
console.log(`\n${test.name}:`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
// Extract namespace declarations
|
|
|
|
|
const namespaceMatches = test.xml.matchAll(/xmlns(?::([^=]+))?="([^"]+)"/g);
|
|
|
|
|
const foundNamespaces = Array.from(namespaceMatches).map(match => ({
|
|
|
|
|
prefix: match[1] || '',
|
|
|
|
|
uri: match[2]
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
console.log(` Expected: ${test.expectedNamespaces.length} namespaces`);
|
|
|
|
|
console.log(` Found: ${foundNamespaces.length} namespaces`);
|
|
|
|
|
|
|
|
|
|
for (const ns of foundNamespaces) {
|
|
|
|
|
console.log(` ${ns.prefix ? `${ns.prefix}:` : '(default)'} ${ns.uri}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
// Verify parsing
|
|
|
|
|
try {
|
|
|
|
|
const invoice = new einvoice.EInvoice();
|
|
|
|
|
await invoice.fromXmlString(test.xml);
|
|
|
|
|
console.log(' ✓ Parsed successfully with namespaces');
|
|
|
|
|
|
|
|
|
|
// Verify the invoice was parsed correctly
|
|
|
|
|
expect(invoice.id).toBeDefined();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ✗ Parse error: ${error.message}`);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('PARSE-05: Namespace Resolution - Namespace scope and inheritance', async () => {
|
|
|
|
|
console.log('\nTesting namespace scope and inheritance...\n');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
const scopeTests = [
|
|
|
|
|
{
|
|
|
|
|
name: 'Namespace inheritance',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
<root xmlns="http://example.com/default">
|
|
|
|
|
<parent>
|
|
|
|
|
<child>Inherits default namespace</child>
|
|
|
|
|
</parent>
|
|
|
|
|
</root>`,
|
2025-05-28 18:46:18 +00:00
|
|
|
|
description: 'Child elements inherit parent namespace'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Namespace override',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
<root xmlns="http://example.com/default">
|
|
|
|
|
<parent>
|
|
|
|
|
<child xmlns="http://example.com/child">Different namespace</child>
|
|
|
|
|
</parent>
|
|
|
|
|
</root>`,
|
2025-05-28 18:46:18 +00:00
|
|
|
|
description: 'Child can override inherited namespace'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'Mixed namespace scopes',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
2025-05-25 19:45:37 +00:00
|
|
|
|
<root xmlns:a="http://example.com/a" xmlns:b="http://example.com/b">
|
|
|
|
|
<a:element1>
|
|
|
|
|
<a:child>Same namespace as parent</a:child>
|
|
|
|
|
<b:child>Different namespace prefix</b:child>
|
|
|
|
|
<unqualified>No namespace prefix</unqualified>
|
|
|
|
|
</a:element1>
|
|
|
|
|
</root>`,
|
2025-05-28 18:46:18 +00:00
|
|
|
|
description: 'Multiple namespace prefixes in scope'
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-28 18:46:18 +00:00
|
|
|
|
];
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
for (const test of scopeTests) {
|
|
|
|
|
console.log(`${test.name}:`);
|
|
|
|
|
console.log(` Description: ${test.description}`);
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const invoice = new einvoice.EInvoice();
|
|
|
|
|
await invoice.fromXmlString(test.xml);
|
|
|
|
|
console.log(' ✓ Namespace scope handled correctly');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
// Expected to fail for non-invoice XML
|
|
|
|
|
console.log(` ℹ Not a valid invoice format (expected)`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-28 18:46:18 +00:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('PARSE-05: Namespace Resolution - Real invoice formats', async () => {
|
|
|
|
|
console.log('\nTesting namespace resolution in real invoice formats...\n');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
const formatTests = [
|
|
|
|
|
{
|
|
|
|
|
name: 'UBL Invoice',
|
|
|
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<ubl:Invoice xmlns:ubl="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>UBL-NS-TEST</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
|
|
|
|
<cac:AccountingSupplierParty>
|
|
|
|
|
<cac:Party>
|
|
|
|
|
<cac:PartyName>
|
|
|
|
|
<cbc:Name>Namespace Test Supplier</cbc:Name>
|
|
|
|
|
</cac:PartyName>
|
|
|
|
|
</cac:Party>
|
|
|
|
|
</cac:AccountingSupplierParty>
|
|
|
|
|
</ubl:Invoice>`,
|
|
|
|
|
expectedFormat: 'UBL'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: 'CII Invoice',
|
|
|
|
|
xml: `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<rsm:CrossIndustryInvoice
|
|
|
|
|
xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
|
|
|
|
|
xmlns:qdt="urn:un:unece:uncefact:data:standard:QualifiedDataType:100"
|
|
|
|
|
xmlns:ram="urn:un:unece:uncefact:data:standard:ReusableAggregateBusinessInformationEntity:100"
|
|
|
|
|
xmlns:xs="http://www.w3.org/2001/XMLSchema"
|
|
|
|
|
xmlns:udt="urn:un:unece:uncefact:data:standard:UnqualifiedDataType:100">
|
|
|
|
|
<rsm:ExchangedDocumentContext>
|
|
|
|
|
<ram:GuidelineSpecifiedDocumentContextParameter>
|
|
|
|
|
<ram:ID>urn:cen.eu:en16931:2017</ram:ID>
|
|
|
|
|
</ram:GuidelineSpecifiedDocumentContextParameter>
|
|
|
|
|
</rsm:ExchangedDocumentContext>
|
|
|
|
|
<rsm:ExchangedDocument>
|
|
|
|
|
<ram:ID>CII-NS-TEST</ram:ID>
|
|
|
|
|
</rsm:ExchangedDocument>
|
|
|
|
|
</rsm:CrossIndustryInvoice>`,
|
|
|
|
|
expectedFormat: 'CII'
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-28 18:46:18 +00:00
|
|
|
|
];
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
for (const test of formatTests) {
|
|
|
|
|
console.log(`${test.name}:`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
try {
|
|
|
|
|
const invoice = new einvoice.EInvoice();
|
|
|
|
|
await invoice.fromXmlString(test.xml);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
console.log(` ✓ Parsed successfully`);
|
|
|
|
|
console.log(` Format: ${invoice.getFormat ? invoice.getFormat() : 'Unknown'}`);
|
|
|
|
|
console.log(` ID: ${invoice.id}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
expect(invoice.id).toBeDefined();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ✗ Parse error: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
2025-05-28 18:46:18 +00:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('PARSE-05: Namespace Resolution - Complex namespace scenarios', async () => {
|
|
|
|
|
console.log('\nTesting complex namespace scenarios...\n');
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
// Test namespace prefix conflicts
|
|
|
|
|
const conflictTest = {
|
|
|
|
|
name: 'Namespace prefix redefinition',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
|
|
|
|
<root xmlns:ns="http://example.com/ns1">
|
|
|
|
|
<ns:element1>Using namespace 1</ns:element1>
|
|
|
|
|
<child xmlns:ns="http://example.com/ns2">
|
|
|
|
|
<ns:element2>Using namespace 2 (redefined)</ns:element2>
|
|
|
|
|
</child>
|
|
|
|
|
</root>`
|
|
|
|
|
};
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
console.log(`${conflictTest.name}:`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
try {
|
|
|
|
|
// Extract all namespace declarations with their scope
|
|
|
|
|
const lines = conflictTest.xml.split('\n');
|
|
|
|
|
let depth = 0;
|
|
|
|
|
|
|
|
|
|
lines.forEach((line, index) => {
|
|
|
|
|
const nsMatch = line.match(/xmlns:(\w+)="([^"]+)"/);
|
|
|
|
|
if (nsMatch) {
|
|
|
|
|
console.log(` Line ${index + 1}: Prefix '${nsMatch[1]}' = ${nsMatch[2]}`);
|
|
|
|
|
}
|
|
|
|
|
});
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
console.log(' ✓ Namespace prefix conflicts are allowed in different scopes');
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ✗ Error: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
// Test empty namespace (undeclaration)
|
|
|
|
|
const undeclarationTest = {
|
|
|
|
|
name: 'Namespace undeclaration',
|
|
|
|
|
xml: `<?xml version="1.0"?>
|
|
|
|
|
<root xmlns="http://example.com/default">
|
|
|
|
|
<parent>
|
|
|
|
|
<child xmlns="">No namespace</child>
|
|
|
|
|
</parent>
|
|
|
|
|
</root>`
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
console.log(`\n${undeclarationTest.name}:`);
|
|
|
|
|
console.log(' Empty xmlns="" removes default namespace from element and children');
|
|
|
|
|
console.log(' ✓ Valid XML construct for namespace undeclaration');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
tap.test('PARSE-05: Namespace Resolution - Performance considerations', async () => {
|
|
|
|
|
console.log('\nTesting namespace resolution performance...\n');
|
|
|
|
|
|
|
|
|
|
// Generate invoice with many namespaces
|
|
|
|
|
const generateComplexNamespaceInvoice = () => {
|
|
|
|
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
|
|
<ubl:Invoice
|
|
|
|
|
xmlns:ubl="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"
|
|
|
|
|
xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"
|
|
|
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
|
|
|
xmlns:xsd="http://www.w3.org/2001/XMLSchema">
|
|
|
|
|
<cbc:ID>PERF-NS-TEST</cbc:ID>
|
|
|
|
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
|
|
|
|
${Array.from({length: 10}, (_, i) => `
|
|
|
|
|
<cac:InvoiceLine>
|
|
|
|
|
<cbc:ID>${i + 1}</cbc:ID>
|
|
|
|
|
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
|
|
|
|
|
<cac:Item>
|
|
|
|
|
<cbc:Name>Item ${i + 1}</cbc:Name>
|
|
|
|
|
<cac:SellersItemIdentification>
|
|
|
|
|
<cbc:ID>ITEM-${i + 1}</cbc:ID>
|
|
|
|
|
</cac:SellersItemIdentification>
|
|
|
|
|
</cac:Item>
|
|
|
|
|
</cac:InvoiceLine>`).join('')}
|
|
|
|
|
</ubl:Invoice>`;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const xml = generateComplexNamespaceInvoice();
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const invoice = new einvoice.EInvoice();
|
|
|
|
|
await invoice.fromXmlString(xml);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
const duration = Date.now() - startTime;
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
console.log('Complex namespace invoice parsing:');
|
|
|
|
|
console.log(` ✓ Parsed successfully in ${duration}ms`);
|
|
|
|
|
console.log(` Invoice ID: ${invoice.id}`);
|
|
|
|
|
console.log(` Line items: ${invoice.items?.length || 0}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
expect(duration).toBeLessThan(100); // Should parse quickly
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.log(` ✗ Parse error: ${error.message}`);
|
2025-05-25 19:45:37 +00:00
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
2025-05-28 18:46:18 +00:00
|
|
|
|
// Run the tests
|
2025-05-25 19:45:37 +00:00
|
|
|
|
tap.start();
|