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();
|
Reference in New Issue
Block a user