369 lines
13 KiB
TypeScript
369 lines
13 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
|
|
tap.test('ENC-04: Character Escaping - should handle XML character escaping correctly', async () => {
|
|
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'
|
|
}
|
|
};
|
|
|
|
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;
|
|
}
|
|
if (content.includes('<') || content.includes('>')) {
|
|
hasUnescapedInContent = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
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=== 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'}`);
|
|
|
|
// 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');
|
|
});
|
|
|
|
tap.start(); |