432 lines
17 KiB
TypeScript
432 lines
17 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from '../plugins.js';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
import { CorpusLoader } from '../corpus.loader.js';
|
|
import { PerformanceTracker } from '../performance.tracker.js';
|
|
|
|
tap.test('ENC-06: Namespace Declarations - should handle XML namespace declarations correctly', async (t) => {
|
|
// ENC-06: Verify proper encoding and handling of XML namespace declarations
|
|
// This test ensures namespace prefixes, URIs, and default namespaces work correctly
|
|
|
|
const performanceTracker = new PerformanceTracker('ENC-06: Namespace Declarations');
|
|
const corpusLoader = new CorpusLoader();
|
|
|
|
t.test('Default namespace declaration', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<UBLVersionID>2.1</UBLVersionID>
|
|
<CustomizationID>urn:cen.eu:en16931:2017</CustomizationID>
|
|
<ID>DEFAULT-NS-TEST</ID>
|
|
<IssueDate>2025-01-25</IssueDate>
|
|
<InvoiceTypeCode>380</InvoiceTypeCode>
|
|
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
|
<AccountingSupplierParty>
|
|
<Party>
|
|
<PartyName>
|
|
<Name>Test Supplier</Name>
|
|
</PartyName>
|
|
</Party>
|
|
</AccountingSupplierParty>
|
|
<AccountingCustomerParty>
|
|
<Party>
|
|
<PartyName>
|
|
<Name>Test Customer</Name>
|
|
</PartyName>
|
|
</Party>
|
|
</AccountingCustomerParty>
|
|
<LegalMonetaryTotal>
|
|
<PayableAmount currencyID="EUR">100.00</PayableAmount>
|
|
</LegalMonetaryTotal>
|
|
</Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify default namespace is preserved
|
|
expect(xmlString).toContain('xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"');
|
|
expect(xmlString).toContain('<Invoice');
|
|
expect(xmlString).toContain('<UBLVersionID>');
|
|
expect(xmlString).not.toContain('xmlns:'); // No prefixed namespaces
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('default-namespace', elapsed);
|
|
});
|
|
|
|
t.test('Multiple namespace declarations', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?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"
|
|
xsi:schemaLocation="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2 UBL-Invoice-2.1.xsd">
|
|
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
|
<cbc:CustomizationID>urn:cen.eu:en16931:2017#conformant#urn:fdc:peppol.eu:2017:poacc:billing:international:peppol:3.0</cbc:CustomizationID>
|
|
<cbc:ProfileID>urn:fdc:peppol.eu:2017:poacc:billing:01:1.0</cbc:ProfileID>
|
|
<cbc:ID>MULTI-NS-TEST</cbc:ID>
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>Namespace Test Supplier</cbc:Name>
|
|
</cac:PartyName>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
<cac:LegalMonetaryTotal>
|
|
<cbc:PayableAmount currencyID="EUR">100.00</cbc:PayableAmount>
|
|
</cac:LegalMonetaryTotal>
|
|
</ubl:Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify all namespace declarations are preserved
|
|
expect(xmlString).toContain('xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"');
|
|
expect(xmlString).toContain('xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"');
|
|
expect(xmlString).toContain('xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"');
|
|
expect(xmlString).toContain('xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2"');
|
|
expect(xmlString).toContain('xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"');
|
|
|
|
// Verify prefixed elements
|
|
expect(xmlString).toContain('<ubl:Invoice');
|
|
expect(xmlString).toContain('<cbc:UBLVersionID>');
|
|
expect(xmlString).toContain('<cac:AccountingSupplierParty>');
|
|
expect(xmlString).toContain('</ubl:Invoice>');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('multiple-namespaces', elapsed);
|
|
});
|
|
|
|
t.test('Nested namespace declarations', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
|
<UBLVersionID>2.1</UBLVersionID>
|
|
<ID>NESTED-NS-TEST</ID>
|
|
<UBLExtensions>
|
|
<UBLExtension>
|
|
<ExtensionContent>
|
|
<sig:UBLDocumentSignatures xmlns:sig="urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2">
|
|
<sac:SignatureInformation xmlns:sac="urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2">
|
|
<cbc:ID xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">SIG-001</cbc:ID>
|
|
<sbc:SignatureMethod xmlns:sbc="urn:oasis:names:specification:ubl:schema:xsd:SignatureBasicComponents-2">RSA-SHA256</sbc:SignatureMethod>
|
|
</sac:SignatureInformation>
|
|
</sig:UBLDocumentSignatures>
|
|
</ExtensionContent>
|
|
</UBLExtension>
|
|
</UBLExtensions>
|
|
<AdditionalDocumentReference>
|
|
<ID>DOC-001</ID>
|
|
<Attachment>
|
|
<EmbeddedDocumentBinaryObject mimeCode="application/pdf" filename="invoice.pdf">
|
|
<xades:QualifyingProperties xmlns:xades="http://uri.etsi.org/01903/v1.3.2#">
|
|
<xades:SignedProperties>
|
|
<xades:SignedSignatureProperties>
|
|
<xades:SigningTime>2025-01-25T10:00:00Z</xades:SigningTime>
|
|
</xades:SignedSignatureProperties>
|
|
</xades:SignedProperties>
|
|
</xades:QualifyingProperties>
|
|
</EmbeddedDocumentBinaryObject>
|
|
</Attachment>
|
|
</AdditionalDocumentReference>
|
|
</Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify nested namespaces are handled correctly
|
|
expect(xmlString).toContain('xmlns:sig="urn:oasis:names:specification:ubl:schema:xsd:CommonSignatureComponents-2"');
|
|
expect(xmlString).toContain('xmlns:sac="urn:oasis:names:specification:ubl:schema:xsd:SignatureAggregateComponents-2"');
|
|
expect(xmlString).toContain('xmlns:xades="http://uri.etsi.org/01903/v1.3.2#"');
|
|
|
|
// Verify nested elements with namespaces
|
|
expect(xmlString).toContain('<sig:UBLDocumentSignatures');
|
|
expect(xmlString).toContain('<sac:SignatureInformation');
|
|
expect(xmlString).toContain('<xades:QualifyingProperties');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('nested-namespaces', elapsed);
|
|
});
|
|
|
|
t.test('Namespace prefixes with special characters', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<inv:Invoice
|
|
xmlns:inv="urn:example:invoice:2.0"
|
|
xmlns:addr-info="urn:example:address:1.0"
|
|
xmlns:pay_terms="urn:example:payment:1.0"
|
|
xmlns:item.details="urn:example:items:1.0">
|
|
<inv:Header>
|
|
<inv:ID>NS-SPECIAL-CHARS</inv:ID>
|
|
<inv:Date>2025-01-25</inv:Date>
|
|
</inv:Header>
|
|
<addr-info:SupplierAddress>
|
|
<addr-info:Name>Test GmbH & Co. KG</addr-info:Name>
|
|
<addr-info:Street>Hauptstraße 42</addr-info:Street>
|
|
<addr-info:City>München</addr-info:City>
|
|
</addr-info:SupplierAddress>
|
|
<pay_terms:PaymentConditions>
|
|
<pay_terms:Terms>Net 30 days</pay_terms:Terms>
|
|
<pay_terms:Discount>2% if < 10 days</pay_terms:Discount>
|
|
</pay_terms:PaymentConditions>
|
|
<item.details:LineItems>
|
|
<item.details:Item>
|
|
<item.details:Description>Product "A" with special chars: €, £, ¥</item.details:Description>
|
|
<item.details:Price currency="EUR">99.99</item.details:Price>
|
|
</item.details:Item>
|
|
</item.details:LineItems>
|
|
</inv:Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify namespace prefixes with hyphens, underscores, dots
|
|
expect(xmlString).toContain('xmlns:addr-info=');
|
|
expect(xmlString).toContain('xmlns:pay_terms=');
|
|
expect(xmlString).toContain('xmlns:item.details=');
|
|
|
|
// Verify elements use correct prefixes
|
|
expect(xmlString).toContain('<addr-info:SupplierAddress');
|
|
expect(xmlString).toContain('<pay_terms:PaymentConditions');
|
|
expect(xmlString).toContain('<item.details:LineItems');
|
|
|
|
// Verify special characters in content are still escaped
|
|
expect(xmlString).toContain('GmbH & Co. KG');
|
|
expect(xmlString).toContain('2% if < 10 days');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('special-prefix-chars', elapsed);
|
|
});
|
|
|
|
t.test('Namespace URI encoding', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice
|
|
xmlns="urn:example:invoice:2.0"
|
|
xmlns:ext="http://example.com/extensions?version=2.0&type=invoice"
|
|
xmlns:intl="http://example.com/i18n/español/facturas"
|
|
xmlns:spec="http://example.com/spec#fragment">
|
|
<ID>URI-ENCODING-TEST</ID>
|
|
<ext:Extension>
|
|
<ext:Type>Custom Extension</ext:Type>
|
|
<ext:Value>Test with encoded URI</ext:Value>
|
|
</ext:Extension>
|
|
<intl:Descripcion>Factura en español</intl:Descripcion>
|
|
<spec:SpecialField>Value with fragment reference</spec:SpecialField>
|
|
</Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify namespace URIs are properly encoded
|
|
expect(xmlString).toContain('xmlns:ext="http://example.com/extensions?version=2.0&type=invoice"');
|
|
expect(xmlString).toContain('xmlns:intl="http://example.com/i18n/español/facturas"');
|
|
expect(xmlString).toContain('xmlns:spec="http://example.com/spec#fragment"');
|
|
|
|
// Verify elements with these namespaces
|
|
expect(xmlString).toContain('<ext:Extension>');
|
|
expect(xmlString).toContain('<intl:Descripcion>');
|
|
expect(xmlString).toContain('<spec:SpecialField>');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('uri-encoding', elapsed);
|
|
});
|
|
|
|
t.test('Namespace inheritance and scoping', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<root:Invoice xmlns:root="urn:example:root:1.0" xmlns:shared="urn:example:shared:1.0">
|
|
<root:Header>
|
|
<shared:ID>NS-SCOPE-TEST</shared:ID>
|
|
<shared:Date>2025-01-25</shared:Date>
|
|
</root:Header>
|
|
<root:Body xmlns:local="urn:example:local:1.0">
|
|
<local:Item>
|
|
<shared:Name>Item using inherited namespace</shared:Name>
|
|
<local:Price>100.00</local:Price>
|
|
</local:Item>
|
|
<root:Subtotal xmlns:calc="urn:example:calc:1.0">
|
|
<calc:Amount>100.00</calc:Amount>
|
|
<calc:Tax rate="19%">19.00</calc:Tax>
|
|
</root:Subtotal>
|
|
</root:Body>
|
|
<root:Footer>
|
|
<!-- local namespace not available here -->
|
|
<shared:Total>119.00</shared:Total>
|
|
</root:Footer>
|
|
</root:Invoice>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// Verify namespace scoping
|
|
expect(xmlString).toContain('xmlns:root="urn:example:root:1.0"');
|
|
expect(xmlString).toContain('xmlns:shared="urn:example:shared:1.0"');
|
|
expect(xmlString).toContain('xmlns:local="urn:example:local:1.0"');
|
|
expect(xmlString).toContain('xmlns:calc="urn:example:calc:1.0"');
|
|
|
|
// Verify proper element prefixing
|
|
expect(xmlString).toContain('<root:Invoice');
|
|
expect(xmlString).toContain('<shared:ID>');
|
|
expect(xmlString).toContain('<local:Item>');
|
|
expect(xmlString).toContain('<calc:Amount>');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('namespace-scoping', elapsed);
|
|
});
|
|
|
|
t.test('Corpus namespace analysis', async () => {
|
|
const startTime = performance.now();
|
|
let processedCount = 0;
|
|
const namespaceStats = {
|
|
defaultNamespace: 0,
|
|
prefixedNamespaces: 0,
|
|
multipleNamespaces: 0,
|
|
commonPrefixes: new Map<string, number>()
|
|
};
|
|
|
|
const files = await corpusLoader.getAllFiles();
|
|
const xmlFiles = files.filter(f => f.endsWith('.xml'));
|
|
|
|
// Analyze namespace usage in corpus
|
|
const sampleSize = Math.min(100, xmlFiles.length);
|
|
const sample = xmlFiles.slice(0, sampleSize);
|
|
|
|
for (const file of sample) {
|
|
try {
|
|
const content = await corpusLoader.readFile(file);
|
|
let xmlString: string;
|
|
|
|
if (Buffer.isBuffer(content)) {
|
|
xmlString = content.toString('utf8');
|
|
} else {
|
|
xmlString = content;
|
|
}
|
|
|
|
// Check for default namespace
|
|
if (/xmlns\s*=\s*["'][^"']+["']/.test(xmlString)) {
|
|
namespaceStats.defaultNamespace++;
|
|
}
|
|
|
|
// Check for prefixed namespaces
|
|
const prefixMatches = xmlString.match(/xmlns:(\w+)\s*=\s*["'][^"']+["']/g);
|
|
if (prefixMatches && prefixMatches.length > 0) {
|
|
namespaceStats.prefixedNamespaces++;
|
|
|
|
if (prefixMatches.length > 2) {
|
|
namespaceStats.multipleNamespaces++;
|
|
}
|
|
|
|
// Count common prefixes
|
|
prefixMatches.forEach(match => {
|
|
const prefixMatch = match.match(/xmlns:(\w+)/);
|
|
if (prefixMatch) {
|
|
const prefix = prefixMatch[1];
|
|
namespaceStats.commonPrefixes.set(
|
|
prefix,
|
|
(namespaceStats.commonPrefixes.get(prefix) || 0) + 1
|
|
);
|
|
}
|
|
});
|
|
}
|
|
|
|
processedCount++;
|
|
} catch (error) {
|
|
console.log(`Namespace parsing issue in ${file}:`, error.message);
|
|
}
|
|
}
|
|
|
|
console.log(`Namespace corpus analysis (${processedCount} files):`);
|
|
console.log(`- Default namespace: ${namespaceStats.defaultNamespace}`);
|
|
console.log(`- Prefixed namespaces: ${namespaceStats.prefixedNamespaces}`);
|
|
console.log(`- Multiple namespaces: ${namespaceStats.multipleNamespaces}`);
|
|
|
|
const topPrefixes = Array.from(namespaceStats.commonPrefixes.entries())
|
|
.sort((a, b) => b[1] - a[1])
|
|
.slice(0, 10);
|
|
console.log('Top namespace prefixes:', topPrefixes);
|
|
|
|
expect(processedCount).toBeGreaterThan(0);
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('corpus-namespaces', elapsed);
|
|
});
|
|
|
|
t.test('Namespace preservation during conversion', async () => {
|
|
const startTime = performance.now();
|
|
|
|
const xmlContent = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<ubl:CreditNote
|
|
xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
|
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
|
xsi:schemaLocation="urn:oasis:names:specification:ubl:schema:xsd:CreditNote-2 UBL-CreditNote-2.1.xsd">
|
|
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
|
<cbc:ID>NS-PRESERVE-TEST</cbc:ID>
|
|
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
|
<cbc:CreditNoteTypeCode>381</cbc:CreditNoteTypeCode>
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>Müller GmbH</cbc:Name>
|
|
</cac:PartyName>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
</ubl:CreditNote>`;
|
|
|
|
const einvoice = new EInvoice();
|
|
await einvoice.loadFromString(xmlContent);
|
|
|
|
// Process and get back
|
|
const xmlString = einvoice.getXmlString();
|
|
|
|
// All original namespaces should be preserved
|
|
expect(xmlString).toContain('xmlns:ubl=');
|
|
expect(xmlString).toContain('xmlns:cac=');
|
|
expect(xmlString).toContain('xmlns:cbc=');
|
|
expect(xmlString).toContain('xmlns:xsi=');
|
|
expect(xmlString).toContain('xsi:schemaLocation=');
|
|
|
|
// Verify namespace prefixes are maintained
|
|
expect(xmlString).toContain('<ubl:CreditNote');
|
|
expect(xmlString).toContain('<cbc:UBLVersionID>');
|
|
expect(xmlString).toContain('<cac:AccountingSupplierParty>');
|
|
expect(xmlString).toContain('</ubl:CreditNote>');
|
|
|
|
const elapsed = performance.now() - startTime;
|
|
performanceTracker.addMeasurement('namespace-preservation', elapsed);
|
|
});
|
|
|
|
// Print performance summary
|
|
performanceTracker.printSummary();
|
|
|
|
// Performance assertions
|
|
const avgTime = performanceTracker.getAverageTime();
|
|
expect(avgTime).toBeLessThan(120); // Namespace operations should be reasonably fast
|
|
});
|
|
|
|
tap.start(); |