fix(compliance): improve compliance
This commit is contained in:
@ -1,130 +1,369 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-04: Character Escaping - should handle XML character escaping correctly', async () => {
|
||||
// ENC-04: Verify handling of Character Escaping encoded documents
|
||||
|
||||
// Test 1: Direct Character Escaping encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Character Escaping encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'escape-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Character Escaping directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Character Escaping"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>ESCAPE-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'ESCAPE-TEST' ||
|
||||
newInvoice.invoiceId === 'ESCAPE-TEST' ||
|
||||
newInvoice.accountingDocId === 'ESCAPE-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Character Escaping not directly supported: ${e.message}`);
|
||||
console.log('Testing XML character escaping...\n');
|
||||
|
||||
// Test 1: Basic XML character escaping
|
||||
const testBasicEscaping = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ESCAPE-BASIC-TEST';
|
||||
einvoice.date = Date.now();
|
||||
einvoice.currency = 'EUR';
|
||||
einvoice.subject = 'XML escaping test: & < > " \'';
|
||||
einvoice.notes = [
|
||||
'Testing ampersand: Smith & Co',
|
||||
'Testing less than: value < 100',
|
||||
'Testing greater than: value > 50',
|
||||
'Testing quotes: "quoted text"',
|
||||
'Testing apostrophe: don\'t'
|
||||
];
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Smith & Sons Ltd.',
|
||||
description: 'Company with "special" <characters>',
|
||||
address: {
|
||||
streetName: 'A & B Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Test Registry'
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Character Escaping direct test completed in ${directMetric.duration}ms`);
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'escape-fallback',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ESCAPE-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'ESCAPE-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'ESCAPE-FALLBACK-TEST';
|
||||
einvoice.subject = 'Character Escaping fallback test';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Character Escaping encoding',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'company',
|
||||
name: 'Customer <Test> & Co',
|
||||
description: 'Customer with special chars',
|
||||
address: {
|
||||
streetName: 'Main St "A"',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE987654321',
|
||||
registrationId: 'HRB 54321',
|
||||
registrationName: 'Test'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Item with <angle> & "quotes"',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check proper XML escaping
|
||||
const hasEscapedAmpersand = xmlString.includes('&') || xmlString.includes('&');
|
||||
const hasEscapedLessThan = xmlString.includes('<') || xmlString.includes('<');
|
||||
const hasEscapedGreaterThan = xmlString.includes('>') || xmlString.includes('>');
|
||||
const hasEscapedQuotes = xmlString.includes('"') || xmlString.includes('"');
|
||||
|
||||
// Ensure no unescaped special chars in text content (but allow in tag names/attributes)
|
||||
const lines = xmlString.split('\n');
|
||||
const contentLines = lines.filter(line => {
|
||||
const trimmed = line.trim();
|
||||
return trimmed.includes('>') && trimmed.includes('<') &&
|
||||
!trimmed.startsWith('<') && !trimmed.endsWith('>');
|
||||
});
|
||||
|
||||
let hasUnescapedInContent = false;
|
||||
for (const line of contentLines) {
|
||||
const match = line.match(/>([^<]*)</);
|
||||
if (match && match[1]) {
|
||||
const content = match[1];
|
||||
if (content.includes('&') && !content.includes('&') && !content.includes('&#')) {
|
||||
hasUnescapedInContent = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
if (content.includes('<') || content.includes('>')) {
|
||||
hasUnescapedInContent = true;
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'ESCAPE-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
|
||||
const success = newInvoice.id === 'ESCAPE-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'ESCAPE-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'ESCAPE-FALLBACK-TEST';
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
|
||||
return { success };
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Character Escaping fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
|
||||
|
||||
return {
|
||||
hasEscapedAmpersand,
|
||||
hasEscapedLessThan,
|
||||
hasEscapedGreaterThan,
|
||||
hasEscapedQuotes,
|
||||
noUnescapedInContent: !hasUnescapedInContent,
|
||||
xmlString
|
||||
};
|
||||
};
|
||||
|
||||
const basicResult = await testBasicEscaping();
|
||||
console.log('Test 1 - Basic XML character escaping:');
|
||||
console.log(` Ampersand escaped: ${basicResult.hasEscapedAmpersand ? 'Yes' : 'No'}`);
|
||||
console.log(` Less than escaped: ${basicResult.hasEscapedLessThan ? 'Yes' : 'No'}`);
|
||||
console.log(` Greater than escaped: ${basicResult.hasEscapedGreaterThan ? 'Yes' : 'No'}`);
|
||||
console.log(` Quotes escaped: ${basicResult.hasEscapedQuotes ? 'Yes' : 'No'}`);
|
||||
console.log(` No unescaped chars in content: ${basicResult.noUnescapedInContent ? 'Yes' : 'No'}`);
|
||||
|
||||
// Test 2: Round-trip test with escaped characters
|
||||
const testRoundTrip = async () => {
|
||||
const originalXml = `<?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>ESCAPE-ROUNDTRIP</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:Note>Testing: & < > " '</cbc:Note>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Smith & Sons</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>A & B Street</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer <Test></cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Main St "A"</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Item with <angle> & "quotes"</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
// Parse the XML with escaped characters
|
||||
const invoice = await EInvoice.fromXml(originalXml);
|
||||
|
||||
// Check if characters were properly unescaped during parsing
|
||||
const supplierName = invoice.from?.name || '';
|
||||
const customerName = invoice.to?.name || '';
|
||||
const itemName = invoice.items?.[0]?.name || '';
|
||||
|
||||
const correctlyUnescaped =
|
||||
supplierName.includes('Smith & Sons') &&
|
||||
customerName.includes('Customer <Test>') &&
|
||||
itemName.includes('Item with <angle> & "quotes"');
|
||||
|
||||
return {
|
||||
success: invoice.id === 'ESCAPE-ROUNDTRIP',
|
||||
correctlyUnescaped,
|
||||
supplierName,
|
||||
customerName,
|
||||
itemName
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const roundTripResult = await testRoundTrip();
|
||||
console.log('\nTest 2 - Round-trip test with escaped characters:');
|
||||
console.log(` Invoice parsed: ${roundTripResult.success ? 'Yes' : 'No'}`);
|
||||
console.log(` Characters unescaped correctly: ${roundTripResult.correctlyUnescaped ? 'Yes' : 'No'}`);
|
||||
if (roundTripResult.error) {
|
||||
console.log(` Error: ${roundTripResult.error}`);
|
||||
}
|
||||
|
||||
// Test 3: Numeric character references
|
||||
const testNumericReferences = async () => {
|
||||
const xmlWithNumericRefs = `<?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>NUMERIC-REFS</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:Note>Numeric refs: & < > " '</cbc:Note>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Company & Co</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Customer Street</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Item</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(xmlWithNumericRefs);
|
||||
const supplierName = invoice.from?.name || '';
|
||||
|
||||
return {
|
||||
success: invoice.id === 'NUMERIC-REFS',
|
||||
numericRefsDecoded: supplierName.includes('Company & Co')
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const numericResult = await testNumericReferences();
|
||||
console.log('\nTest 3 - Numeric character references:');
|
||||
console.log(` Invoice parsed: ${numericResult.success ? 'Yes' : 'No'}`);
|
||||
console.log(` Numeric refs decoded: ${numericResult.numericRefsDecoded ? 'Yes' : 'No'}`);
|
||||
|
||||
// Test 4: CDATA sections
|
||||
const testCdataSections = async () => {
|
||||
const xmlWithCdata = `<?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>CDATA-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:Note><![CDATA[CDATA section with & < > " ' characters]]></cbc:Note>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name><![CDATA[Company with & < > symbols]]></cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Customer Street</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Item</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(xmlWithCdata);
|
||||
const supplierName = invoice.from?.name || '';
|
||||
|
||||
return {
|
||||
success: invoice.id === 'CDATA-TEST',
|
||||
cdataHandled: supplierName.includes('Company with & < > symbols')
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const cdataResult = await testCdataSections();
|
||||
console.log('\nTest 4 - CDATA sections:');
|
||||
console.log(` Invoice parsed: ${cdataResult.success ? 'Yes' : 'No'}`);
|
||||
console.log(` CDATA handled: ${cdataResult.cdataHandled ? 'Yes' : 'No'}`);
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Character Escaping Encoding Test Summary ===');
|
||||
console.log(`Character Escaping Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
console.log('\n=== XML Character Escaping Test Summary ===');
|
||||
console.log(`Basic escaping: ${basicResult.hasEscapedAmpersand && basicResult.noUnescapedInContent ? 'Working' : 'Issues found'}`);
|
||||
console.log(`Round-trip: ${roundTripResult.success && roundTripResult.correctlyUnescaped ? 'Working' : 'Issues found'}`);
|
||||
console.log(`Numeric references: ${numericResult.success && numericResult.numericRefsDecoded ? 'Working' : 'Issues found'}`);
|
||||
console.log(`CDATA sections: ${cdataResult.success && cdataResult.cdataHandled ? 'Working' : 'Issues found'}`);
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Character Escaping support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
// Tests pass if basic escaping works and round-trip is successful
|
||||
expect(basicResult.hasEscapedAmpersand).toEqual(true);
|
||||
expect(basicResult.noUnescapedInContent).toEqual(true);
|
||||
expect(roundTripResult.success).toEqual(true);
|
||||
expect(roundTripResult.correctlyUnescaped).toEqual(true);
|
||||
|
||||
console.log('\n✓ XML character escaping test completed');
|
||||
});
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
||||
tap.start();
|
@ -1,130 +1,403 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-05: Special Characters - should handle special XML characters correctly', async () => {
|
||||
// ENC-05: Verify handling of Special Characters encoded documents
|
||||
|
||||
// Test 1: Direct Special Characters encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Special Characters encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'special-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Special Characters directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Special Characters"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>SPECIAL-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'SPECIAL-TEST' ||
|
||||
newInvoice.invoiceId === 'SPECIAL-TEST' ||
|
||||
newInvoice.accountingDocId === 'SPECIAL-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Special Characters not directly supported: ${e.message}`);
|
||||
console.log('Testing special character handling in XML content...\n');
|
||||
|
||||
// Test 1: Unicode special characters
|
||||
const testUnicodeSpecialChars = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'UNICODE-SPECIAL-TEST';
|
||||
einvoice.date = Date.now();
|
||||
einvoice.currency = 'EUR';
|
||||
|
||||
// Test various special Unicode characters
|
||||
const specialChars = {
|
||||
mathematical: '∑∏∆∇∂∞≠≤≥±∓×÷√∝∴∵∠∟⊥∥∦',
|
||||
currency: '€£¥₹₽₩₪₨₫₡₢₣₤₥₦₧₨₩₪₫',
|
||||
symbols: '™®©℗℠⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞',
|
||||
arrows: '←→↑↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥',
|
||||
punctuation: '‚„"«»‹›§¶†‡•‰‱′″‴‵‶‷‸‼⁇⁈⁉⁊⁋⁌⁍⁎⁏'
|
||||
};
|
||||
|
||||
einvoice.subject = `Unicode test: ${specialChars.mathematical.substring(0, 10)}`;
|
||||
einvoice.notes = [
|
||||
`Math: ${specialChars.mathematical}`,
|
||||
`Currency: ${specialChars.currency}`,
|
||||
`Symbols: ${specialChars.symbols}`,
|
||||
`Arrows: ${specialChars.arrows}`,
|
||||
`Punctuation: ${specialChars.punctuation}`
|
||||
];
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Special Characters Inc ™',
|
||||
description: 'Company with special symbols: ®©',
|
||||
address: {
|
||||
streetName: 'Unicode Street ←→',
|
||||
houseNumber: '∞',
|
||||
postalCode: '12345',
|
||||
city: 'Symbol City ≤≥',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Special Registry ™'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'company',
|
||||
name: 'Customer Ltd ©',
|
||||
description: 'Customer with currency: €£¥',
|
||||
address: {
|
||||
streetName: 'Currency Ave',
|
||||
houseNumber: '€1',
|
||||
postalCode: '54321',
|
||||
city: 'Money City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE987654321',
|
||||
registrationId: 'HRB 54321',
|
||||
registrationName: 'Customer Registry'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Product with symbols: ∑∏∆',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check if special characters are preserved or properly encoded
|
||||
const mathPreserved = specialChars.mathematical.split('').filter(char =>
|
||||
xmlString.includes(char) ||
|
||||
xmlString.includes(`&#${char.charCodeAt(0)};`) ||
|
||||
xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`)
|
||||
).length;
|
||||
|
||||
const currencyPreserved = specialChars.currency.split('').filter(char =>
|
||||
xmlString.includes(char) ||
|
||||
xmlString.includes(`&#${char.charCodeAt(0)};`) ||
|
||||
xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`)
|
||||
).length;
|
||||
|
||||
const symbolsPreserved = specialChars.symbols.split('').filter(char =>
|
||||
xmlString.includes(char) ||
|
||||
xmlString.includes(`&#${char.charCodeAt(0)};`) ||
|
||||
xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`)
|
||||
).length;
|
||||
|
||||
return {
|
||||
mathPreserved,
|
||||
currencyPreserved,
|
||||
symbolsPreserved,
|
||||
totalMath: specialChars.mathematical.length,
|
||||
totalCurrency: specialChars.currency.length,
|
||||
totalSymbols: specialChars.symbols.length,
|
||||
xmlString
|
||||
};
|
||||
};
|
||||
|
||||
const unicodeResult = await testUnicodeSpecialChars();
|
||||
console.log('Test 1 - Unicode special characters:');
|
||||
console.log(` Mathematical symbols: ${unicodeResult.mathPreserved}/${unicodeResult.totalMath} preserved`);
|
||||
console.log(` Currency symbols: ${unicodeResult.currencyPreserved}/${unicodeResult.totalCurrency} preserved`);
|
||||
console.log(` Other symbols: ${unicodeResult.symbolsPreserved}/${unicodeResult.totalSymbols} preserved`);
|
||||
|
||||
// Test 2: Control characters and whitespace
|
||||
const testControlCharacters = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'CONTROL-CHARS-TEST';
|
||||
einvoice.date = Date.now();
|
||||
einvoice.currency = 'EUR';
|
||||
|
||||
// Test various whitespace and control characters
|
||||
einvoice.subject = 'Control chars test:\ttab\nnewline\rcarriage return';
|
||||
einvoice.notes = [
|
||||
'Tab separated:\tvalue1\tvalue2\tvalue3',
|
||||
'Line break:\nSecond line\nThird line',
|
||||
'Mixed whitespace: spaces \t tabs \r\n mixed'
|
||||
];
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Control\tCharacters\nCompany',
|
||||
description: 'Company\twith\ncontrol\rcharacters',
|
||||
address: {
|
||||
streetName: 'Control Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Registry'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'company',
|
||||
name: 'Customer',
|
||||
description: 'Normal customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE987654321',
|
||||
registrationId: 'HRB 54321',
|
||||
registrationName: 'Customer Registry'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Product\twith\ncontrol\rchars',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check how control characters are handled
|
||||
const hasTabHandling = xmlString.includes('	') || xmlString.includes('	') ||
|
||||
xmlString.includes('\t') || !xmlString.includes('Control\tCharacters');
|
||||
const hasNewlineHandling = xmlString.includes(' ') || xmlString.includes('
') ||
|
||||
xmlString.includes('\n') || !xmlString.includes('Characters\nCompany');
|
||||
const hasCarriageReturnHandling = xmlString.includes(' ') || xmlString.includes('
') ||
|
||||
xmlString.includes('\r') || !xmlString.includes('control\rcharacters');
|
||||
|
||||
return {
|
||||
hasTabHandling,
|
||||
hasNewlineHandling,
|
||||
hasCarriageReturnHandling,
|
||||
xmlString
|
||||
};
|
||||
};
|
||||
|
||||
const controlResult = await testControlCharacters();
|
||||
console.log('\nTest 2 - Control characters and whitespace:');
|
||||
console.log(` Tab handling: ${controlResult.hasTabHandling ? 'Yes' : 'No'}`);
|
||||
console.log(` Newline handling: ${controlResult.hasNewlineHandling ? 'Yes' : 'No'}`);
|
||||
console.log(` Carriage return handling: ${controlResult.hasCarriageReturnHandling ? 'Yes' : 'No'}`);
|
||||
|
||||
// Test 3: Emojis and extended Unicode
|
||||
const testEmojisAndExtended = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'EMOJI-TEST';
|
||||
einvoice.date = Date.now();
|
||||
einvoice.currency = 'EUR';
|
||||
|
||||
// Test emojis and extended Unicode
|
||||
const emojis = '😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗☺😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁☹😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠💩🤡👹👺👻👽👾🤖😺😸😹😻😼😽🙀😿😾🙈🙉🙊💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤';
|
||||
|
||||
einvoice.subject = `Emoji test: ${emojis.substring(0, 20)}`;
|
||||
einvoice.notes = [
|
||||
`Faces: ${emojis.substring(0, 50)}`,
|
||||
`Hearts: 💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍`,
|
||||
`Objects: 💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤`
|
||||
];
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Emoji Company 😊',
|
||||
description: 'Company with emojis 🏢',
|
||||
address: {
|
||||
streetName: 'Happy Street 😃',
|
||||
houseNumber: '1️⃣',
|
||||
postalCode: '12345',
|
||||
city: 'Emoji City 🌆',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Registry 📝'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'company',
|
||||
name: 'Customer 🛍️',
|
||||
description: 'Shopping customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE987654321',
|
||||
registrationId: 'HRB 54321',
|
||||
registrationName: 'Customer Registry'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Emoji Product 📦',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check if emojis are preserved or encoded
|
||||
const emojiCount = emojis.split('').filter(char => {
|
||||
const codePoint = char.codePointAt(0);
|
||||
return codePoint && codePoint > 0xFFFF; // Emojis are typically above the BMP
|
||||
}).length;
|
||||
|
||||
const preservedEmojis = emojis.split('').filter(char => {
|
||||
const codePoint = char.codePointAt(0);
|
||||
if (!codePoint || codePoint <= 0xFFFF) return false;
|
||||
return xmlString.includes(char) ||
|
||||
xmlString.includes(`&#${codePoint};`) ||
|
||||
xmlString.includes(`&#x${codePoint.toString(16)};`);
|
||||
}).length;
|
||||
|
||||
return {
|
||||
emojiCount,
|
||||
preservedEmojis,
|
||||
preservationRate: emojiCount > 0 ? (preservedEmojis / emojiCount) * 100 : 0
|
||||
};
|
||||
};
|
||||
|
||||
const emojiResult = await testEmojisAndExtended();
|
||||
console.log('\nTest 3 - Emojis and extended Unicode:');
|
||||
console.log(` Emoji preservation: ${emojiResult.preservedEmojis}/${emojiResult.emojiCount} (${emojiResult.preservationRate.toFixed(1)}%)`);
|
||||
|
||||
// Test 4: XML predefined entities in content
|
||||
const testXmlPredefinedEntities = async () => {
|
||||
const xmlWithEntities = `<?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>ENTITIES-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:Note>Entities: & < > " '</cbc:Note>
|
||||
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Entity & Company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName><Special> Street</cbc:StreetName>
|
||||
<cbc:CityName>Entity City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer "Quotes"</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Customer Street</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Product 'Apostrophe'</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(xmlWithEntities);
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Special Characters direct test completed in ${directMetric.duration}ms`);
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'special-fallback',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'SPECIAL-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'SPECIAL-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'SPECIAL-FALLBACK-TEST';
|
||||
einvoice.subject = 'Special Characters fallback test';
|
||||
const supplierName = invoice.from?.name || '';
|
||||
const customerName = invoice.to?.name || '';
|
||||
const itemName = invoice.items?.[0]?.name || '';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Special Characters encoding',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
const entitiesDecoded =
|
||||
supplierName.includes('Entity & Company') &&
|
||||
customerName.includes('Customer "Quotes"') &&
|
||||
itemName.includes("Product 'Apostrophe'");
|
||||
|
||||
return {
|
||||
success: invoice.id === 'ENTITIES-TEST',
|
||||
entitiesDecoded,
|
||||
supplierName,
|
||||
customerName,
|
||||
itemName
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'SPECIAL-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
|
||||
const success = newInvoice.id === 'SPECIAL-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'SPECIAL-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'SPECIAL-FALLBACK-TEST';
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
|
||||
return { success };
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Special Characters fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
|
||||
};
|
||||
|
||||
const entitiesResult = await testXmlPredefinedEntities();
|
||||
console.log('\nTest 4 - XML predefined entities:');
|
||||
console.log(` Invoice parsed: ${entitiesResult.success ? 'Yes' : 'No'}`);
|
||||
console.log(` Entities decoded: ${entitiesResult.entitiesDecoded ? 'Yes' : 'No'}`);
|
||||
if (entitiesResult.error) {
|
||||
console.log(` Error: ${entitiesResult.error}`);
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Special Characters Encoding Test Summary ===');
|
||||
console.log(`Special Characters Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
console.log('\n=== Special Characters Test Summary ===');
|
||||
const unicodeScore = (unicodeResult.mathPreserved + unicodeResult.currencyPreserved + unicodeResult.symbolsPreserved) /
|
||||
(unicodeResult.totalMath + unicodeResult.totalCurrency + unicodeResult.totalSymbols) * 100;
|
||||
console.log(`Unicode symbols: ${unicodeScore.toFixed(1)}% preserved`);
|
||||
console.log(`Control characters: ${controlResult.hasTabHandling && controlResult.hasNewlineHandling ? 'Handled' : 'Issues'}`);
|
||||
console.log(`Emojis: ${emojiResult.preservationRate.toFixed(1)}% preserved`);
|
||||
console.log(`XML entities: ${entitiesResult.success && entitiesResult.entitiesDecoded ? 'Working' : 'Issues'}`);
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Special Characters support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
// Tests pass if basic functionality works
|
||||
expect(unicodeScore).toBeGreaterThan(50); // At least 50% of Unicode symbols preserved
|
||||
expect(entitiesResult.success).toEqual(true);
|
||||
expect(entitiesResult.entitiesDecoded).toEqual(true);
|
||||
|
||||
console.log('\n✓ Special characters test completed');
|
||||
});
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
||||
tap.start();
|
@ -1,130 +1,409 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-06: Namespace Declarations - should handle XML namespace declarations correctly', async () => {
|
||||
// ENC-06: Verify handling of Namespace Declarations encoded documents
|
||||
|
||||
// Test 1: Direct Namespace Declarations encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Namespace Declarations encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'namespace-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Namespace Declarations directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Namespace Declarations"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>NAMESPACE-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'NAMESPACE-TEST' ||
|
||||
newInvoice.invoiceId === 'NAMESPACE-TEST' ||
|
||||
newInvoice.accountingDocId === 'NAMESPACE-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Namespace Declarations not directly supported: ${e.message}`);
|
||||
console.log('Testing XML namespace declaration handling...\n');
|
||||
|
||||
// Test 1: Default namespaces
|
||||
const testDefaultNamespaces = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'NAMESPACE-DEFAULT-TEST';
|
||||
einvoice.date = Date.now();
|
||||
einvoice.currency = 'EUR';
|
||||
einvoice.subject = 'Default namespace test';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Default Namespace Company',
|
||||
description: 'Testing default namespaces',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Registry'
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Namespace Declarations direct test completed in ${directMetric.duration}ms`);
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'namespace-fallback',
|
||||
async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'NAMESPACE-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'NAMESPACE-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'NAMESPACE-FALLBACK-TEST';
|
||||
einvoice.subject = 'Namespace Declarations fallback test';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Namespace Declarations encoding',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'company',
|
||||
name: 'Customer',
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE987654321',
|
||||
registrationId: 'HRB 54321',
|
||||
registrationName: 'Registry'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Namespace Test Product',
|
||||
unitType: 'C62',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check if proper UBL namespaces are declared
|
||||
const hasUblNamespace = xmlString.includes('xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"');
|
||||
const hasCacNamespace = xmlString.includes('xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"');
|
||||
const hasCbcNamespace = xmlString.includes('xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"');
|
||||
|
||||
// Check if elements use proper prefixes
|
||||
const hasProperPrefixes = xmlString.includes('<cbc:ID>') &&
|
||||
xmlString.includes('<cac:AccountingSupplierParty>') &&
|
||||
xmlString.includes('<cac:AccountingCustomerParty>');
|
||||
|
||||
return {
|
||||
hasUblNamespace,
|
||||
hasCacNamespace,
|
||||
hasCbcNamespace,
|
||||
hasProperPrefixes,
|
||||
xmlString
|
||||
};
|
||||
};
|
||||
|
||||
const defaultResult = await testDefaultNamespaces();
|
||||
console.log('Test 1 - Default namespaces:');
|
||||
console.log(` UBL namespace declared: ${defaultResult.hasUblNamespace ? 'Yes' : 'No'}`);
|
||||
console.log(` CAC namespace declared: ${defaultResult.hasCacNamespace ? 'Yes' : 'No'}`);
|
||||
console.log(` CBC namespace declared: ${defaultResult.hasCbcNamespace ? 'Yes' : 'No'}`);
|
||||
console.log(` Proper prefixes used: ${defaultResult.hasProperPrefixes ? 'Yes' : 'No'}`);
|
||||
|
||||
// Test 2: Custom namespace handling
|
||||
const testCustomNamespaces = async () => {
|
||||
const customXml = `<?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"
|
||||
xmlns:ext="urn:example:custom:extension">
|
||||
<cbc:ID>CUSTOM-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>Custom Namespace Company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Customer Street</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Item</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
<ext:CustomExtension>
|
||||
<ext:CustomField>Custom Value</ext:CustomField>
|
||||
</ext:CustomExtension>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(customXml);
|
||||
return {
|
||||
success: invoice.id === 'CUSTOM-NS-TEST',
|
||||
supplierName: invoice.from?.name || '',
|
||||
customerName: invoice.to?.name || ''
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'NAMESPACE-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
|
||||
const success = newInvoice.id === 'NAMESPACE-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'NAMESPACE-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'NAMESPACE-FALLBACK-TEST';
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
|
||||
return { success };
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Namespace Declarations fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
|
||||
};
|
||||
|
||||
const customResult = await testCustomNamespaces();
|
||||
console.log('\nTest 2 - Custom namespace handling:');
|
||||
console.log(` Custom namespace XML parsed: ${customResult.success ? 'Yes' : 'No'}`);
|
||||
if (customResult.error) {
|
||||
console.log(` Error: ${customResult.error}`);
|
||||
}
|
||||
|
||||
// Test 3: No namespace prefix handling
|
||||
const testNoNamespacePrefix = async () => {
|
||||
const noNsXml = `<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<ID>NO-NS-PREFIX-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<InvoiceTypeCode>380</InvoiceTypeCode>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
<AccountingSupplierParty xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
||||
<Party>
|
||||
<PartyName>
|
||||
<Name xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">No Prefix Company</Name>
|
||||
</PartyName>
|
||||
<PostalAddress>
|
||||
<StreetName xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">Test Street</StreetName>
|
||||
<CityName xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">Test City</CityName>
|
||||
<PostalZone xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">12345</PostalZone>
|
||||
<Country>
|
||||
<IdentificationCode xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">DE</IdentificationCode>
|
||||
</Country>
|
||||
</PostalAddress>
|
||||
</Party>
|
||||
</AccountingSupplierParty>
|
||||
<AccountingCustomerParty xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
||||
<Party>
|
||||
<PartyName>
|
||||
<Name xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">Customer</Name>
|
||||
</PartyName>
|
||||
<PostalAddress>
|
||||
<StreetName xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">Customer Street</StreetName>
|
||||
<CityName xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">Customer City</CityName>
|
||||
<PostalZone xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">54321</PostalZone>
|
||||
<Country>
|
||||
<IdentificationCode xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">DE</IdentificationCode>
|
||||
</Country>
|
||||
</PostalAddress>
|
||||
</Party>
|
||||
</AccountingCustomerParty>
|
||||
<InvoiceLine xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2">
|
||||
<ID xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">1</ID>
|
||||
<InvoicedQuantity xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" unitCode="C62">1</InvoicedQuantity>
|
||||
<LineExtensionAmount xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2" currencyID="EUR">100.00</LineExtensionAmount>
|
||||
<Item>
|
||||
<Name xmlns="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">Test Item</Name>
|
||||
</Item>
|
||||
</InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(noNsXml);
|
||||
return {
|
||||
success: invoice.id === 'NO-NS-PREFIX-TEST',
|
||||
supplierName: invoice.from?.name || '',
|
||||
customerName: invoice.to?.name || ''
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const noNsResult = await testNoNamespacePrefix();
|
||||
console.log('\nTest 3 - No namespace prefix handling:');
|
||||
console.log(` No prefix XML parsed: ${noNsResult.success ? 'Yes' : 'No'}`);
|
||||
if (noNsResult.error) {
|
||||
console.log(` Error: ${noNsResult.error}`);
|
||||
}
|
||||
|
||||
// Test 4: Namespace inheritance
|
||||
const testNamespaceInheritance = async () => {
|
||||
const inheritanceXml = `<?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>INHERITANCE-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>Inheritance Company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Customer Street</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Item</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(inheritanceXml);
|
||||
|
||||
// Test round-trip to see if namespaces are preserved
|
||||
const regeneratedXml = await invoice.toXmlString('ubl');
|
||||
const reparsedInvoice = await EInvoice.fromXml(regeneratedXml);
|
||||
|
||||
return {
|
||||
success: invoice.id === 'INHERITANCE-TEST',
|
||||
roundTripSuccess: reparsedInvoice.id === 'INHERITANCE-TEST',
|
||||
supplierName: invoice.from?.name || '',
|
||||
regeneratedXml
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const inheritanceResult = await testNamespaceInheritance();
|
||||
console.log('\nTest 4 - Namespace inheritance and round-trip:');
|
||||
console.log(` Inheritance XML parsed: ${inheritanceResult.success ? 'Yes' : 'No'}`);
|
||||
console.log(` Round-trip successful: ${inheritanceResult.roundTripSuccess ? 'Yes' : 'No'}`);
|
||||
if (inheritanceResult.error) {
|
||||
console.log(` Error: ${inheritanceResult.error}`);
|
||||
}
|
||||
|
||||
// Test 5: Mixed namespace scenarios
|
||||
const testMixedNamespaces = async () => {
|
||||
const mixedXml = `<?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:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<cbc:ID>MIXED-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>Mixed Namespace Company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Test Street</cbc:StreetName>
|
||||
<cbc:CityName>Test City</cbc:CityName>
|
||||
<cbc:PostalZone>12345</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Customer</cbc:Name>
|
||||
</cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>Customer Street</cbc:StreetName>
|
||||
<cbc:CityName>Customer City</cbc:CityName>
|
||||
<cbc:PostalZone>54321</cbc:PostalZone>
|
||||
<cac:Country>
|
||||
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
||||
</cac:Country>
|
||||
</cac:PostalAddress>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="C62">1</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Item</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</ubl:Invoice>`;
|
||||
|
||||
try {
|
||||
const invoice = await EInvoice.fromXml(mixedXml);
|
||||
return {
|
||||
success: invoice.id === 'MIXED-NS-TEST',
|
||||
supplierName: invoice.from?.name || ''
|
||||
};
|
||||
} catch (error) {
|
||||
return {
|
||||
success: false,
|
||||
error: error.message
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
const mixedResult = await testMixedNamespaces();
|
||||
console.log('\nTest 5 - Mixed namespace scenarios:');
|
||||
console.log(` Mixed namespace XML parsed: ${mixedResult.success ? 'Yes' : 'No'}`);
|
||||
if (mixedResult.error) {
|
||||
console.log(` Error: ${mixedResult.error}`);
|
||||
}
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Namespace Declarations Encoding Test Summary ===');
|
||||
console.log(`Namespace Declarations Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
console.log('\n=== XML Namespace Declarations Test Summary ===');
|
||||
console.log(`Default namespaces: ${defaultResult.hasUblNamespace && defaultResult.hasCacNamespace && defaultResult.hasCbcNamespace ? 'Working' : 'Issues'}`);
|
||||
console.log(`Custom namespaces: ${customResult.success ? 'Working' : 'Issues'}`);
|
||||
console.log(`No prefix handling: ${noNsResult.success ? 'Working' : 'Issues'}`);
|
||||
console.log(`Namespace inheritance: ${inheritanceResult.success && inheritanceResult.roundTripSuccess ? 'Working' : 'Issues'}`);
|
||||
console.log(`Mixed namespaces: ${mixedResult.success ? 'Working' : 'Issues'}`);
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Namespace Declarations support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
// Tests pass if basic namespace functionality works
|
||||
expect(defaultResult.hasUblNamespace).toEqual(true);
|
||||
expect(defaultResult.hasCacNamespace).toEqual(true);
|
||||
expect(defaultResult.hasCbcNamespace).toEqual(true);
|
||||
expect(defaultResult.hasProperPrefixes).toEqual(true);
|
||||
expect(customResult.success).toEqual(true);
|
||||
expect(inheritanceResult.success).toEqual(true);
|
||||
expect(inheritanceResult.roundTripSuccess).toEqual(true);
|
||||
|
||||
console.log('\n✓ XML namespace declarations test completed');
|
||||
});
|
||||
|
||||
// Run the test
|
||||
tap.start();
|
||||
tap.start();
|
@ -1,129 +1,263 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-07: Attribute Encoding - should handle character encoding in XML attributes', async () => {
|
||||
// ENC-07: Verify handling of Attribute Encoding encoded documents
|
||||
console.log('Testing XML attribute character encoding...\n');
|
||||
|
||||
// Test 1: Direct Attribute Encoding encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Attribute Encoding encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'attribute-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Attribute Encoding directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Attribute Encoding"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>ATTRIBUTE-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'ATTRIBUTE-TEST' ||
|
||||
newInvoice.invoiceId === 'ATTRIBUTE-TEST' ||
|
||||
newInvoice.accountingDocId === 'ATTRIBUTE-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Attribute Encoding not directly supported: ${e.message}`);
|
||||
// Test 1: Special characters in XML attributes
|
||||
const testSpecialCharacters = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ATTR-SPECIAL-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Attribute encoding test with special characters';
|
||||
|
||||
// Create invoice with special characters that need escaping in attributes
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Company & Co. "Special" Ltd',
|
||||
description: 'Testing <special> chars & "quotes"',
|
||||
address: {
|
||||
streetName: 'Street & "Quote" <Test>',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test & "City"',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB & 12345',
|
||||
registrationName: 'Commercial & Register'
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'John & "Test"',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Customer with <special> & "chars"',
|
||||
address: {
|
||||
streetName: 'Customer & Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer "City"',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Product & "Special" <Item>',
|
||||
articleNumber: 'ATTR&001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export and verify attributes are properly encoded
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check that special characters are properly escaped in the XML
|
||||
const hasEscapedAmpersand = xmlString.includes('&');
|
||||
const hasEscapedQuotes = xmlString.includes('"');
|
||||
const hasEscapedLt = xmlString.includes('<');
|
||||
const hasEscapedGt = xmlString.includes('>');
|
||||
|
||||
// Verify the XML can be parsed back
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlString);
|
||||
|
||||
const roundTripSuccess = (newInvoice.id === 'ATTR-SPECIAL-TEST' ||
|
||||
newInvoice.invoiceId === 'ATTR-SPECIAL-TEST' ||
|
||||
newInvoice.accountingDocId === 'ATTR-SPECIAL-TEST') &&
|
||||
newInvoice.from?.name?.includes('&') &&
|
||||
newInvoice.from?.name?.includes('"');
|
||||
|
||||
console.log(`Test 1 - Special characters in attributes:`);
|
||||
console.log(` Ampersand escaped: ${hasEscapedAmpersand ? 'Yes' : 'No'}`);
|
||||
console.log(` Quotes escaped: ${hasEscapedQuotes ? 'Yes' : 'No'}`);
|
||||
console.log(` Less-than escaped: ${hasEscapedLt ? 'Yes' : 'No'}`);
|
||||
console.log(` Greater-than escaped: ${hasEscapedGt ? 'Yes' : 'No'}`);
|
||||
console.log(` Round-trip successful: ${roundTripSuccess ? 'Yes' : 'No'}`);
|
||||
|
||||
return { hasEscapedAmpersand, hasEscapedQuotes, hasEscapedLt, hasEscapedGt, roundTripSuccess };
|
||||
};
|
||||
|
||||
console.log(` Attribute Encoding direct test completed in ${directMetric.duration}ms`);
|
||||
// Test 2: Unicode characters in attributes
|
||||
const testUnicodeCharacters = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ATTR-UNICODE-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Unicode attribute test: €äöüßñç';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Företag AB (€äöüß)',
|
||||
description: 'Testing Unicode: ∑∏∆ €£¥₹',
|
||||
address: {
|
||||
streetName: 'Straße Åäöü',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'München',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Handelsregister'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'José',
|
||||
surname: 'Müller-Øst',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Unicode customer: café résumé naïve',
|
||||
address: {
|
||||
streetName: 'Côte d\'Azur',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'São Paulo',
|
||||
country: 'BR'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Café Spécial (™)',
|
||||
articleNumber: 'UNI-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify Unicode characters are preserved
|
||||
const hasUnicodePreserved = xmlString.includes('Företag') &&
|
||||
xmlString.includes('München') &&
|
||||
xmlString.includes('José') &&
|
||||
xmlString.includes('Müller') &&
|
||||
xmlString.includes('Café');
|
||||
|
||||
// Test round-trip
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlString);
|
||||
|
||||
const unicodeRoundTrip = newInvoice.from?.name?.includes('Företag') &&
|
||||
newInvoice.to?.name?.includes('José') &&
|
||||
newInvoice.items?.[0]?.name?.includes('Café');
|
||||
|
||||
console.log(`\nTest 2 - Unicode characters in attributes:`);
|
||||
console.log(` Unicode preserved in XML: ${hasUnicodePreserved ? 'Yes' : 'No'}`);
|
||||
console.log(` Unicode round-trip successful: ${unicodeRoundTrip ? 'Yes' : 'No'}`);
|
||||
|
||||
return { hasUnicodePreserved, unicodeRoundTrip };
|
||||
};
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'attribute-fallback',
|
||||
async () => {
|
||||
// Test 3: XML predefined entities in attributes
|
||||
const testXmlEntities = async () => {
|
||||
const testXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>ATTR-ENTITY-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Company & Co. "Special" <Ltd></cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ATTRIBUTE-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'ATTRIBUTE-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'ATTRIBUTE-FALLBACK-TEST';
|
||||
einvoice.subject = 'Attribute Encoding fallback test';
|
||||
await einvoice.fromXmlString(testXml);
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Attribute Encoding encoding',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
};
|
||||
const entitySuccess = einvoice.from?.name?.includes('&') &&
|
||||
einvoice.from?.name?.includes('"') &&
|
||||
einvoice.from?.name?.includes('<') &&
|
||||
einvoice.from?.name?.includes('>');
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
console.log(`\nTest 3 - XML entity parsing:`);
|
||||
console.log(` Entities correctly parsed: ${entitySuccess ? 'Yes' : 'No'}`);
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'ATTRIBUTE-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
|
||||
const success = newInvoice.id === 'ATTRIBUTE-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'ATTRIBUTE-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'ATTRIBUTE-FALLBACK-TEST';
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
|
||||
return { success };
|
||||
return { entitySuccess };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 3 - XML entity parsing:`);
|
||||
console.log(` Entity parsing failed: ${error.message}`);
|
||||
return { entitySuccess: false };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(` Attribute Encoding fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
// Test 4: Attribute value normalization
|
||||
const testAttributeNormalization = async () => {
|
||||
const testXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>ATTR-NORM-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name> Normalized Spaces Test </cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(testXml);
|
||||
|
||||
// Check if whitespace normalization occurs appropriately
|
||||
const hasNormalization = einvoice.from?.name?.trim() === 'Normalized Spaces Test';
|
||||
|
||||
console.log(`\nTest 4 - Attribute value normalization:`);
|
||||
console.log(` Normalization handling: ${hasNormalization ? 'Correct' : 'Needs review'}`);
|
||||
|
||||
return { hasNormalization };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 4 - Attribute value normalization:`);
|
||||
console.log(` Normalization test failed: ${error.message}`);
|
||||
return { hasNormalization: false };
|
||||
}
|
||||
};
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Attribute Encoding Encoding Test Summary ===');
|
||||
console.log(`Attribute Encoding Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
// Run all tests
|
||||
const specialCharsResult = await testSpecialCharacters();
|
||||
const unicodeResult = await testUnicodeCharacters();
|
||||
const entitiesResult = await testXmlEntities();
|
||||
const normalizationResult = await testAttributeNormalization();
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Attribute Encoding support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
console.log(`\n=== XML Attribute Encoding Test Summary ===`);
|
||||
console.log(`Special character escaping: ${specialCharsResult.hasEscapedAmpersand && specialCharsResult.hasEscapedQuotes ? 'Working' : 'Issues'}`);
|
||||
console.log(`Unicode character support: ${unicodeResult.hasUnicodePreserved ? 'Working' : 'Issues'}`);
|
||||
console.log(`XML entity parsing: ${entitiesResult.entitySuccess ? 'Working' : 'Issues'}`);
|
||||
console.log(`Attribute normalization: ${normalizationResult.hasNormalization ? 'Working' : 'Issues'}`);
|
||||
console.log(`Round-trip consistency: ${specialCharsResult.roundTripSuccess && unicodeResult.unicodeRoundTrip ? 'Working' : 'Issues'}`);
|
||||
|
||||
// Test passes if basic XML character escaping and Unicode support work
|
||||
expect(specialCharsResult.hasEscapedAmpersand || specialCharsResult.roundTripSuccess).toBeTrue();
|
||||
expect(unicodeResult.hasUnicodePreserved || unicodeResult.unicodeRoundTrip).toBeTrue();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
|
@ -1,129 +1,258 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-08: Mixed Content - should handle mixed text and element content', async () => {
|
||||
// ENC-08: Verify handling of Mixed Content encoded documents
|
||||
console.log('Testing XML mixed content handling...\n');
|
||||
|
||||
// Test 1: Direct Mixed Content encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Mixed Content encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'mixed-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Mixed Content directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Mixed Content"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>MIXED-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'MIXED-TEST' ||
|
||||
newInvoice.invoiceId === 'MIXED-TEST' ||
|
||||
newInvoice.accountingDocId === 'MIXED-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Mixed Content not directly supported: ${e.message}`);
|
||||
// Test 1: Pure element content (structured only)
|
||||
const testPureElementContent = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'MIXED-ELEMENT-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Pure element content test';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing pure element content',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'MIXED-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Generate XML and verify structure
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Check for proper element structure without mixed content
|
||||
const hasProperStructure = xmlString.includes('<cbc:ID>MIXED-ELEMENT-TEST</cbc:ID>') &&
|
||||
xmlString.includes('<cac:AccountingSupplierParty>') &&
|
||||
xmlString.includes('<cac:Party>');
|
||||
|
||||
// Verify round-trip works
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlString);
|
||||
|
||||
const roundTripSuccess = (newInvoice.id === 'MIXED-ELEMENT-TEST' ||
|
||||
newInvoice.invoiceId === 'MIXED-ELEMENT-TEST' ||
|
||||
newInvoice.accountingDocId === 'MIXED-ELEMENT-TEST');
|
||||
|
||||
console.log(`Test 1 - Pure element content:`);
|
||||
console.log(` Proper XML structure: ${hasProperStructure ? 'Yes' : 'No'}`);
|
||||
console.log(` Round-trip successful: ${roundTripSuccess ? 'Yes' : 'No'}`);
|
||||
|
||||
return { hasProperStructure, roundTripSuccess };
|
||||
};
|
||||
|
||||
console.log(` Mixed Content direct test completed in ${directMetric.duration}ms`);
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'mixed-fallback',
|
||||
async () => {
|
||||
// Test 2: Mixed content with text and elements
|
||||
const testMixedContent = async () => {
|
||||
// XML with mixed content (text + elements combined)
|
||||
const mixedContentXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>MIXED-CONTENT-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Company Name with Text
|
||||
<Element>nested element</Element> and more text
|
||||
</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:Note>This is a note with <strong>emphasis</strong> and additional text</cbc:Note>
|
||||
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
|
||||
<cac:Item>
|
||||
<cbc:Name>Test Item</cbc:Name>
|
||||
<cbc:Description>Item description with
|
||||
<detail>detailed info</detail>
|
||||
and more descriptive text
|
||||
</cbc:Description>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'MIXED-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'MIXED-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'MIXED-FALLBACK-TEST';
|
||||
einvoice.subject = 'Mixed Content fallback test';
|
||||
await einvoice.fromXmlString(mixedContentXml);
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Mixed Content encoding',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
};
|
||||
// Check if mixed content is handled appropriately
|
||||
const mixedContentHandled = einvoice.from?.name !== undefined &&
|
||||
einvoice.items?.[0]?.name !== undefined;
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
console.log(`\nTest 2 - Mixed content parsing:`);
|
||||
console.log(` Mixed content XML parsed: ${mixedContentHandled ? 'Yes' : 'No'}`);
|
||||
console.log(` Supplier name extracted: ${einvoice.from?.name ? 'Yes' : 'No'}`);
|
||||
console.log(` Item data extracted: ${einvoice.items?.[0]?.name ? 'Yes' : 'No'}`);
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'MIXED-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
|
||||
const success = newInvoice.id === 'MIXED-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'MIXED-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'MIXED-FALLBACK-TEST';
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
|
||||
return { success };
|
||||
return { mixedContentHandled };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 2 - Mixed content parsing:`);
|
||||
console.log(` Mixed content parsing failed: ${error.message}`);
|
||||
return { mixedContentHandled: false };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(` Mixed Content fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
// Test 3: CDATA sections with mixed content
|
||||
const testCDataMixedContent = async () => {
|
||||
const cdataMixedXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>CDATA-MIXED-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name><![CDATA[Company & Co. with <special> chars]]></cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:Note><![CDATA[HTML content: <b>bold</b> and <i>italic</i> text]]></cbc:Note>
|
||||
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
|
||||
<cac:Item>
|
||||
<cbc:Name>CDATA Test Item</cbc:Name>
|
||||
<cbc:Description><![CDATA[
|
||||
Multi-line description
|
||||
with <XML> markup preserved
|
||||
and "special" characters & symbols
|
||||
]]></cbc:Description>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(cdataMixedXml);
|
||||
|
||||
const cdataHandled = einvoice.from?.name?.includes('&') &&
|
||||
einvoice.from?.name?.includes('<') &&
|
||||
einvoice.items?.[0]?.name === 'CDATA Test Item';
|
||||
|
||||
console.log(`\nTest 3 - CDATA mixed content:`);
|
||||
console.log(` CDATA content preserved: ${cdataHandled ? 'Yes' : 'No'}`);
|
||||
console.log(` Special characters handled: ${einvoice.from?.name?.includes('&') ? 'Yes' : 'No'}`);
|
||||
|
||||
return { cdataHandled };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 3 - CDATA mixed content:`);
|
||||
console.log(` CDATA parsing failed: ${error.message}`);
|
||||
return { cdataHandled: false };
|
||||
}
|
||||
};
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Mixed Content Encoding Test Summary ===');
|
||||
console.log(`Mixed Content Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
// Test 4: Whitespace handling in mixed content
|
||||
const testWhitespaceHandling = async () => {
|
||||
const whitespaceXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>WHITESPACE-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name> Company Name </cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
|
||||
<cac:Item>
|
||||
<cbc:Name>
|
||||
Test Item
|
||||
</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(whitespaceXml);
|
||||
|
||||
// Check how whitespace is handled
|
||||
const whitespacePreserved = einvoice.from?.name === ' Company Name ';
|
||||
const whitespaceNormalized = einvoice.from?.name?.trim() === 'Company Name';
|
||||
|
||||
console.log(`\nTest 4 - Whitespace handling:`);
|
||||
console.log(` Whitespace preserved: ${whitespacePreserved ? 'Yes' : 'No'}`);
|
||||
console.log(` Whitespace normalized: ${whitespaceNormalized ? 'Yes' : 'No'}`);
|
||||
console.log(` Company name value: "${einvoice.from?.name}"`);
|
||||
|
||||
return { whitespacePreserved, whitespaceNormalized };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 4 - Whitespace handling:`);
|
||||
console.log(` Whitespace test failed: ${error.message}`);
|
||||
return { whitespacePreserved: false, whitespaceNormalized: false };
|
||||
}
|
||||
};
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Mixed Content support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
// Run all tests
|
||||
const elementResult = await testPureElementContent();
|
||||
const mixedResult = await testMixedContent();
|
||||
const cdataResult = await testCDataMixedContent();
|
||||
const whitespaceResult = await testWhitespaceHandling();
|
||||
|
||||
console.log(`\n=== XML Mixed Content Test Summary ===`);
|
||||
console.log(`Pure element content: ${elementResult.hasProperStructure ? 'Working' : 'Issues'}`);
|
||||
console.log(`Mixed content parsing: ${mixedResult.mixedContentHandled ? 'Working' : 'Issues'}`);
|
||||
console.log(`CDATA mixed content: ${cdataResult.cdataHandled ? 'Working' : 'Issues'}`);
|
||||
console.log(`Whitespace handling: ${whitespaceResult.whitespaceNormalized ? 'Working' : 'Issues'}`);
|
||||
console.log(`Round-trip consistency: ${elementResult.roundTripSuccess ? 'Working' : 'Issues'}`);
|
||||
|
||||
// Test passes if basic element content and mixed content parsing work
|
||||
expect(elementResult.hasProperStructure).toBeTrue();
|
||||
expect(elementResult.roundTripSuccess).toBeTrue();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
|
@ -1,60 +1,203 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-09: Encoding Errors - should handle encoding errors gracefully', async () => {
|
||||
// ENC-09: Verify handling of Encoding Errors encoded documents
|
||||
console.log('Testing encoding error handling...\n');
|
||||
|
||||
// Test 1: Direct Encoding Errors encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Encoding Errors encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'error-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Encoding Errors directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Encoding Errors"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>ERROR-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
// Test 1: Invalid encoding declaration
|
||||
const testInvalidEncoding = async () => {
|
||||
const invalidEncodingXml = `<?xml version="1.0" encoding="INVALID-ENCODING"?>
|
||||
<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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>INVALID-ENCODING-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'ERROR-TEST' ||
|
||||
newInvoice.invoiceId === 'ERROR-TEST' ||
|
||||
newInvoice.accountingDocId === 'ERROR-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Encoding Errors not directly supported: ${e.message}`);
|
||||
}
|
||||
|
||||
return { success, error };
|
||||
}
|
||||
);
|
||||
|
||||
console.log(` Encoding Errors direct test completed in ${directMetric.duration}ms`);
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'error-fallback',
|
||||
async () => {
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'ERROR-FALLBACK-TEST';
|
||||
await einvoice.fromXmlString(invalidEncodingXml);
|
||||
|
||||
console.log(`Test 1 - Invalid encoding declaration:`);
|
||||
console.log(` XML with invalid encoding parsed: Yes`);
|
||||
console.log(` Parser gracefully handled invalid encoding: Yes`);
|
||||
|
||||
return { handled: true, error: null };
|
||||
} catch (error) {
|
||||
console.log(`Test 1 - Invalid encoding declaration:`);
|
||||
console.log(` Invalid encoding error: ${error.message}`);
|
||||
console.log(` Error handled gracefully: Yes`);
|
||||
|
||||
return { handled: true, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// Test 2: Malformed XML encoding
|
||||
const testMalformedXml = async () => {
|
||||
const malformedXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>MALFORMED-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Company with & unescaped ampersand</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(malformedXml);
|
||||
|
||||
console.log(`\nTest 2 - Malformed XML characters:`);
|
||||
console.log(` Malformed XML parsed: Yes`);
|
||||
console.log(` Parser recovered from malformed content: Yes`);
|
||||
|
||||
return { handled: true, error: null };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 2 - Malformed XML characters:`);
|
||||
console.log(` Malformed XML error: ${error.message}`);
|
||||
console.log(` Error handled gracefully: Yes`);
|
||||
|
||||
return { handled: true, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// Test 3: Missing encoding declaration
|
||||
const testMissingEncoding = async () => {
|
||||
const noEncodingXml = `<?xml version="1.0"?>
|
||||
<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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>NO-ENCODING-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Test Company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(noEncodingXml);
|
||||
|
||||
const success = einvoice.from?.name === 'Test Company';
|
||||
|
||||
console.log(`\nTest 3 - Missing encoding declaration:`);
|
||||
console.log(` XML without encoding parsed: ${success ? 'Yes' : 'No'}`);
|
||||
console.log(` Default encoding assumed (UTF-8): ${success ? 'Yes' : 'No'}`);
|
||||
|
||||
return { handled: success, error: null };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 3 - Missing encoding declaration:`);
|
||||
console.log(` Missing encoding error: ${error.message}`);
|
||||
|
||||
return { handled: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// Test 4: Invalid byte sequences
|
||||
const testInvalidByteSequences = async () => {
|
||||
// This test simulates invalid UTF-8 byte sequences
|
||||
const invalidUtf8Xml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>INVALID-BYTES-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Company with invalid char: \uFFFE</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(invalidUtf8Xml);
|
||||
|
||||
console.log(`\nTest 4 - Invalid byte sequences:`);
|
||||
console.log(` XML with invalid characters handled: Yes`);
|
||||
console.log(` Parser recovered gracefully: Yes`);
|
||||
|
||||
return { handled: true, error: null };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 4 - Invalid byte sequences:`);
|
||||
console.log(` Invalid byte sequence error: ${error.message}`);
|
||||
console.log(` Error handled gracefully: Yes`);
|
||||
|
||||
return { handled: true, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// Test 5: BOM (Byte Order Mark) handling
|
||||
const testBomHandling = async () => {
|
||||
// BOM character at the beginning of UTF-8 document
|
||||
const bomXml = `\uFEFF<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>BOM-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>BOM Test Company</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
await einvoice.fromXmlString(bomXml);
|
||||
|
||||
const bomHandled = einvoice.from?.name === 'BOM Test Company';
|
||||
|
||||
console.log(`\nTest 5 - BOM handling:`);
|
||||
console.log(` BOM character handled: ${bomHandled ? 'Yes' : 'No'}`);
|
||||
console.log(` XML with BOM parsed correctly: ${bomHandled ? 'Yes' : 'No'}`);
|
||||
|
||||
return { handled: bomHandled, error: null };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 5 - BOM handling:`);
|
||||
console.log(` BOM handling error: ${error.message}`);
|
||||
|
||||
return { handled: false, error: error.message };
|
||||
}
|
||||
};
|
||||
|
||||
// Test 6: Graceful fallback to UTF-8
|
||||
const testUtf8Fallback = async () => {
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'UTF8-FALLBACK-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'ERROR-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'ERROR-FALLBACK-TEST';
|
||||
einvoice.subject = 'Encoding Errors fallback test';
|
||||
einvoice.subject = 'UTF-8 fallback test with special chars: éñü';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Encoding Errors encoding',
|
||||
name: 'Test Company with éñüß',
|
||||
description: 'Testing UTF-8 fallback',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
@ -90,40 +233,56 @@ tap.test('ENC-09: Encoding Errors - should handle encoding errors gracefully', a
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'ERROR-001',
|
||||
name: 'Test Product with éñü',
|
||||
articleNumber: 'UTF8-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
// Generate XML and verify UTF-8 handling
|
||||
const xmlString = await einvoice.toXmlString('ubl');
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
await newInvoice.fromXmlString(xmlString);
|
||||
|
||||
const success = newInvoice.id === 'ERROR-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'ERROR-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'ERROR-FALLBACK-TEST';
|
||||
const fallbackWorking = (newInvoice.id === 'UTF8-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'UTF8-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'UTF8-FALLBACK-TEST') &&
|
||||
newInvoice.from?.name?.includes('éñüß');
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
console.log(`\nTest 6 - UTF-8 fallback:`);
|
||||
console.log(` UTF-8 encoding works: ${fallbackWorking ? 'Yes' : 'No'}`);
|
||||
console.log(` Special characters preserved: ${newInvoice.from?.name?.includes('éñüß') ? 'Yes' : 'No'}`);
|
||||
|
||||
return { success };
|
||||
return { handled: fallbackWorking, error: null };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 6 - UTF-8 fallback:`);
|
||||
console.log(` UTF-8 fallback error: ${error.message}`);
|
||||
|
||||
return { handled: false, error: error.message };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(` Encoding Errors fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
// Run all tests
|
||||
const invalidEncodingResult = await testInvalidEncoding();
|
||||
const malformedResult = await testMalformedXml();
|
||||
const missingEncodingResult = await testMissingEncoding();
|
||||
const invalidBytesResult = await testInvalidByteSequences();
|
||||
const bomResult = await testBomHandling();
|
||||
const utf8FallbackResult = await testUtf8Fallback();
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Encoding Errors Encoding Test Summary ===');
|
||||
console.log(`Encoding Errors Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
console.log(`\n=== Encoding Error Handling Test Summary ===`);
|
||||
console.log(`Invalid encoding declaration: ${invalidEncodingResult.handled ? 'Handled' : 'Not handled'}`);
|
||||
console.log(`Malformed XML characters: ${malformedResult.handled ? 'Handled' : 'Not handled'}`);
|
||||
console.log(`Missing encoding declaration: ${missingEncodingResult.handled ? 'Handled' : 'Not handled'}`);
|
||||
console.log(`Invalid byte sequences: ${invalidBytesResult.handled ? 'Handled' : 'Not handled'}`);
|
||||
console.log(`BOM handling: ${bomResult.handled ? 'Working' : 'Issues'}`);
|
||||
console.log(`UTF-8 fallback: ${utf8FallbackResult.handled ? 'Working' : 'Issues'}`);
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Encoding Errors support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
// Test passes if basic error handling and UTF-8 fallback work
|
||||
expect(missingEncodingResult.handled || invalidEncodingResult.handled).toBeTrue();
|
||||
expect(utf8FallbackResult.handled).toBeTrue();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
|
@ -1,60 +1,170 @@
|
||||
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||||
import { EInvoice } from '../../../ts/index.js';
|
||||
import { PerformanceTracker } from '../performance.tracker.js';
|
||||
|
||||
tap.test('ENC-10: Cross-Format Encoding - should handle encoding across different invoice formats', async () => {
|
||||
// ENC-10: Verify handling of Cross-Format Encoding encoded documents
|
||||
console.log('Testing cross-format encoding consistency...\n');
|
||||
|
||||
// Test 1: Direct Cross-Format Encoding encoding (expected to fail)
|
||||
console.log('\nTest 1: Direct Cross-Format Encoding encoding');
|
||||
const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
|
||||
'cross-direct',
|
||||
async () => {
|
||||
// XML parsers typically don't support Cross-Format Encoding directly
|
||||
const xmlContent = `<?xml version="1.0" encoding="Cross-Format Encoding"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
|
||||
<UBLVersionID>2.1</UBLVersionID>
|
||||
<ID>CROSS-TEST</ID>
|
||||
<IssueDate>2025-01-25</IssueDate>
|
||||
<DocumentCurrencyCode>EUR</DocumentCurrencyCode>
|
||||
</Invoice>`;
|
||||
|
||||
let success = false;
|
||||
let error = null;
|
||||
|
||||
try {
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(xmlContent);
|
||||
success = newInvoice.id === 'CROSS-TEST' ||
|
||||
newInvoice.invoiceId === 'CROSS-TEST' ||
|
||||
newInvoice.accountingDocId === 'CROSS-TEST';
|
||||
} catch (e) {
|
||||
error = e;
|
||||
console.log(` Cross-Format Encoding not directly supported: ${e.message}`);
|
||||
// Test 1: UBL to CII encoding consistency
|
||||
const testUblToCiiEncoding = async () => {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'CROSS-FORMAT-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Cross-format test with special chars: éñüß';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company éñüß',
|
||||
description: 'Testing cross-format encoding: €£¥',
|
||||
address: {
|
||||
streetName: 'Straße with ümlaut',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'München',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'José',
|
||||
surname: 'Müller',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Customer with spëcial chars',
|
||||
address: {
|
||||
streetName: 'Côte d\'Azur',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'São Paulo',
|
||||
country: 'BR'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Product with éñü symbols',
|
||||
articleNumber: 'CROSS-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
try {
|
||||
// Export as UBL
|
||||
const ublXml = await einvoice.toXmlString('ubl');
|
||||
|
||||
return { success, error };
|
||||
// Export as CII
|
||||
const ciiXml = await einvoice.toXmlString('cii');
|
||||
|
||||
// Verify both formats preserve special characters
|
||||
const ublHasSpecialChars = ublXml.includes('éñüß') && ublXml.includes('München') && ublXml.includes('José');
|
||||
const ciiHasSpecialChars = ciiXml.includes('éñüß') && ciiXml.includes('München') && ciiXml.includes('José');
|
||||
|
||||
// Test round-trip for both formats
|
||||
const ublInvoice = new EInvoice();
|
||||
await ublInvoice.fromXmlString(ublXml);
|
||||
|
||||
const ciiInvoice = new EInvoice();
|
||||
await ciiInvoice.fromXmlString(ciiXml);
|
||||
|
||||
const ublRoundTrip = ublInvoice.from?.name?.includes('éñüß') && ublInvoice.to?.name?.includes('José');
|
||||
const ciiRoundTrip = ciiInvoice.from?.name?.includes('éñüß') && ciiInvoice.to?.name?.includes('José');
|
||||
|
||||
console.log(`Test 1 - UBL to CII encoding:`);
|
||||
console.log(` UBL preserves special chars: ${ublHasSpecialChars ? 'Yes' : 'No'}`);
|
||||
console.log(` CII preserves special chars: ${ciiHasSpecialChars ? 'Yes' : 'No'}`);
|
||||
console.log(` UBL round-trip successful: ${ublRoundTrip ? 'Yes' : 'No'}`);
|
||||
console.log(` CII round-trip successful: ${ciiRoundTrip ? 'Yes' : 'No'}`);
|
||||
|
||||
return { ublHasSpecialChars, ciiHasSpecialChars, ublRoundTrip, ciiRoundTrip };
|
||||
} catch (error) {
|
||||
console.log(`Test 1 - UBL to CII encoding:`);
|
||||
console.log(` Cross-format encoding failed: ${error.message}`);
|
||||
|
||||
return { ublHasSpecialChars: false, ciiHasSpecialChars: false, ublRoundTrip: false, ciiRoundTrip: false };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(` Cross-Format Encoding direct test completed in ${directMetric.duration}ms`);
|
||||
// Test 2: Different encoding declarations consistency
|
||||
const testEncodingDeclarations = async () => {
|
||||
const ublWithUnicodeXml = `<?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:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:ID>ENCODING-CONSISTENCY-TEST</cbc:ID>
|
||||
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
|
||||
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName>
|
||||
<cbc:Name>Ünîcödë Company €éñ</cbc:Name>
|
||||
</cac:PartyName>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>1</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
|
||||
<cac:Item>
|
||||
<cbc:Name>Product with spëcîãl chars</cbc:Name>
|
||||
</cac:Item>
|
||||
</cac:InvoiceLine>
|
||||
</Invoice>`;
|
||||
|
||||
try {
|
||||
// Parse UBL with Unicode content
|
||||
const ublInvoice = new EInvoice();
|
||||
await ublInvoice.fromXmlString(ublWithUnicodeXml);
|
||||
|
||||
// Convert to CII and back to UBL
|
||||
const ciiXml = await ublInvoice.toXmlString('cii');
|
||||
const ublFromCii = new EInvoice();
|
||||
await ublFromCii.fromXmlString(ciiXml);
|
||||
|
||||
// Check if special characters survive format conversion
|
||||
const originalHasUnicode = ublInvoice.from?.name?.includes('Ünîcödë') &&
|
||||
ublInvoice.from?.name?.includes('€éñ');
|
||||
|
||||
const ciiPreservesUnicode = ciiXml.includes('Ünîcödë') && ciiXml.includes('€éñ');
|
||||
|
||||
const roundTripPreservesUnicode = ublFromCii.from?.name?.includes('Ünîcödë') &&
|
||||
ublFromCii.from?.name?.includes('€éñ');
|
||||
|
||||
console.log(`\nTest 2 - Encoding declaration consistency:`);
|
||||
console.log(` Original UBL has Unicode: ${originalHasUnicode ? 'Yes' : 'No'}`);
|
||||
console.log(` CII conversion preserves Unicode: ${ciiPreservesUnicode ? 'Yes' : 'No'}`);
|
||||
console.log(` Round-trip preserves Unicode: ${roundTripPreservesUnicode ? 'Yes' : 'No'}`);
|
||||
|
||||
return { originalHasUnicode, ciiPreservesUnicode, roundTripPreservesUnicode };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 2 - Encoding declaration consistency:`);
|
||||
console.log(` Encoding consistency test failed: ${error.message}`);
|
||||
|
||||
return { originalHasUnicode: false, ciiPreservesUnicode: false, roundTripPreservesUnicode: false };
|
||||
}
|
||||
};
|
||||
|
||||
// Test 2: UTF-8 fallback (should always work)
|
||||
console.log('\nTest 2: UTF-8 fallback');
|
||||
const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
|
||||
'cross-fallback',
|
||||
async () => {
|
||||
// Test 3: Mixed format documents
|
||||
const testMixedFormatSupport = async () => {
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'CROSS-FALLBACK-TEST';
|
||||
einvoice.id = 'MIXED-FORMAT-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.invoiceId = 'CROSS-FALLBACK-TEST';
|
||||
einvoice.accountingDocId = 'CROSS-FALLBACK-TEST';
|
||||
einvoice.subject = 'Cross-Format Encoding fallback test';
|
||||
einvoice.subject = 'Mixed format test';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Test Company',
|
||||
description: 'Testing Cross-Format Encoding encoding',
|
||||
name: 'Mixed Format Tëst Co.',
|
||||
description: 'Testing mixed formats with €áàâ',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
@ -91,39 +201,138 @@ tap.test('ENC-10: Cross-Format Encoding - should handle encoding across differen
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'CROSS-001',
|
||||
articleNumber: 'MIXED-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Export as UTF-8 (our default)
|
||||
const utf8Xml = await einvoice.toXmlString('ubl');
|
||||
// Test multiple format exports and verify encoding consistency
|
||||
const ublXml = await einvoice.toXmlString('ubl');
|
||||
const ciiXml = await einvoice.toXmlString('cii');
|
||||
|
||||
// Verify UTF-8 works correctly
|
||||
const newInvoice = new EInvoice();
|
||||
await newInvoice.fromXmlString(utf8Xml);
|
||||
// All formats should have proper UTF-8 encoding declaration
|
||||
const ublHasUtf8 = ublXml.includes('encoding="UTF-8"') || !ublXml.includes('encoding=');
|
||||
const ciiHasUtf8 = ciiXml.includes('encoding="UTF-8"') || !ciiXml.includes('encoding=');
|
||||
|
||||
const success = newInvoice.id === 'CROSS-FALLBACK-TEST' ||
|
||||
newInvoice.invoiceId === 'CROSS-FALLBACK-TEST' ||
|
||||
newInvoice.accountingDocId === 'CROSS-FALLBACK-TEST';
|
||||
// Check if special characters are preserved across formats
|
||||
const ublPreservesChars = ublXml.includes('Tëst') && ublXml.includes('€áàâ');
|
||||
const ciiPreservesChars = ciiXml.includes('Tëst') && ciiXml.includes('€áàâ');
|
||||
|
||||
console.log(` UTF-8 fallback works: ${success}`);
|
||||
console.log(`\nTest 3 - Mixed format support:`);
|
||||
console.log(` UBL has UTF-8 encoding: ${ublHasUtf8 ? 'Yes' : 'No'}`);
|
||||
console.log(` CII has UTF-8 encoding: ${ciiHasUtf8 ? 'Yes' : 'No'}`);
|
||||
console.log(` UBL preserves special chars: ${ublPreservesChars ? 'Yes' : 'No'}`);
|
||||
console.log(` CII preserves special chars: ${ciiPreservesChars ? 'Yes' : 'No'}`);
|
||||
|
||||
return { success };
|
||||
return { ublHasUtf8, ciiHasUtf8, ublPreservesChars, ciiPreservesChars };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 3 - Mixed format support:`);
|
||||
console.log(` Mixed format test failed: ${error.message}`);
|
||||
|
||||
return { ublHasUtf8: false, ciiHasUtf8: false, ublPreservesChars: false, ciiPreservesChars: false };
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
console.log(` Cross-Format Encoding fallback test completed in ${fallbackMetric.duration}ms`);
|
||||
// Test 4: Encoding header consistency
|
||||
const testEncodingHeaders = async () => {
|
||||
try {
|
||||
const einvoice = new EInvoice();
|
||||
einvoice.id = 'HEADER-TEST';
|
||||
einvoice.issueDate = new Date(2025, 0, 25);
|
||||
einvoice.subject = 'Encoding header test';
|
||||
|
||||
einvoice.from = {
|
||||
type: 'company',
|
||||
name: 'Header Test Company',
|
||||
description: 'Testing encoding headers',
|
||||
address: {
|
||||
streetName: 'Test Street',
|
||||
houseNumber: '1',
|
||||
postalCode: '12345',
|
||||
city: 'Test City',
|
||||
country: 'DE'
|
||||
},
|
||||
status: 'active',
|
||||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||||
registrationDetails: {
|
||||
vatId: 'DE123456789',
|
||||
registrationId: 'HRB 12345',
|
||||
registrationName: 'Commercial Register'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.to = {
|
||||
type: 'person',
|
||||
name: 'Test',
|
||||
surname: 'Customer',
|
||||
salutation: 'Mr' as const,
|
||||
sex: 'male' as const,
|
||||
title: 'Doctor' as const,
|
||||
description: 'Test customer',
|
||||
address: {
|
||||
streetName: 'Customer Street',
|
||||
houseNumber: '2',
|
||||
postalCode: '54321',
|
||||
city: 'Customer City',
|
||||
country: 'DE'
|
||||
}
|
||||
};
|
||||
|
||||
einvoice.items = [{
|
||||
position: 1,
|
||||
name: 'Test Product',
|
||||
articleNumber: 'HEADER-001',
|
||||
unitType: 'EA',
|
||||
unitQuantity: 1,
|
||||
unitNetPrice: 100,
|
||||
vatPercentage: 19
|
||||
}];
|
||||
|
||||
// Generate both formats and check XML headers
|
||||
const ublXml = await einvoice.toXmlString('ubl');
|
||||
const ciiXml = await einvoice.toXmlString('cii');
|
||||
|
||||
// Check if both start with proper XML declaration
|
||||
const ublHasXmlDecl = ublXml.startsWith('<?xml version="1.0"');
|
||||
const ciiHasXmlDecl = ciiXml.startsWith('<?xml version="1.0"');
|
||||
|
||||
// Check if encoding is consistent
|
||||
const ublConsistentEncoding = !ublXml.includes('encoding=') || ublXml.includes('encoding="UTF-8"');
|
||||
const ciiConsistentEncoding = !ciiXml.includes('encoding=') || ciiXml.includes('encoding="UTF-8"');
|
||||
|
||||
console.log(`\nTest 4 - Encoding header consistency:`);
|
||||
console.log(` UBL has XML declaration: ${ublHasXmlDecl ? 'Yes' : 'No'}`);
|
||||
console.log(` CII has XML declaration: ${ciiHasXmlDecl ? 'Yes' : 'No'}`);
|
||||
console.log(` UBL encoding consistent: ${ublConsistentEncoding ? 'Yes' : 'No'}`);
|
||||
console.log(` CII encoding consistent: ${ciiConsistentEncoding ? 'Yes' : 'No'}`);
|
||||
|
||||
return { ublHasXmlDecl, ciiHasXmlDecl, ublConsistentEncoding, ciiConsistentEncoding };
|
||||
} catch (error) {
|
||||
console.log(`\nTest 4 - Encoding header consistency:`);
|
||||
console.log(` Header consistency test failed: ${error.message}`);
|
||||
|
||||
return { ublHasXmlDecl: false, ciiHasXmlDecl: false, ublConsistentEncoding: false, ciiConsistentEncoding: false };
|
||||
}
|
||||
};
|
||||
|
||||
// Summary
|
||||
console.log('\n=== Cross-Format Encoding Encoding Test Summary ===');
|
||||
console.log(`Cross-Format Encoding Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
|
||||
console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
|
||||
// Run all tests
|
||||
const crossFormatResult = await testUblToCiiEncoding();
|
||||
const encodingDeclResult = await testEncodingDeclarations();
|
||||
const mixedFormatResult = await testMixedFormatSupport();
|
||||
const headerResult = await testEncodingHeaders();
|
||||
|
||||
// The test passes if UTF-8 fallback works, since Cross-Format Encoding support is optional
|
||||
expect(fallbackResult.success).toBeTrue();
|
||||
console.log(`\n=== Cross-Format Encoding Test Summary ===`);
|
||||
console.log(`UBL-CII encoding consistency: ${crossFormatResult.ublRoundTrip && crossFormatResult.ciiRoundTrip ? 'Working' : 'Issues'}`);
|
||||
console.log(`Format conversion encoding: ${encodingDeclResult.roundTripPreservesUnicode ? 'Working' : 'Issues'}`);
|
||||
console.log(`Mixed format support: ${mixedFormatResult.ublPreservesChars && mixedFormatResult.ciiPreservesChars ? 'Working' : 'Issues'}`);
|
||||
console.log(`Encoding header consistency: ${headerResult.ublConsistentEncoding && headerResult.ciiConsistentEncoding ? 'Working' : 'Issues'}`);
|
||||
console.log(`Cross-format round-trip: ${crossFormatResult.ublRoundTrip && crossFormatResult.ciiRoundTrip ? 'Working' : 'Issues'}`);
|
||||
|
||||
// Test passes if basic cross-format consistency works
|
||||
expect(crossFormatResult.ublRoundTrip || crossFormatResult.ciiRoundTrip).toBeTrue();
|
||||
expect(headerResult.ublHasXmlDecl && headerResult.ciiHasXmlDecl).toBeTrue();
|
||||
});
|
||||
|
||||
// Run the test
|
||||
|
Reference in New Issue
Block a user