import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as plugins from '../plugins.js'; import { EInvoice } from '../../../ts/index.js'; import { CorpusLoader } from '../corpus.loader.js'; import { PerformanceTracker } from '../performance.tracker.js'; tap.test('ENC-04: Character Escaping - should handle XML character escaping correctly', async (t) => { // ENC-04: Verify proper escaping and unescaping of special XML characters // This test ensures XML entities and special characters are handled correctly const performanceTracker = new PerformanceTracker('ENC-04: Character Escaping'); const corpusLoader = new CorpusLoader(); t.test('Basic XML entity escaping', async () => { const startTime = performance.now(); // Test the five predefined XML entities const xmlContent = ` 2.1 ESCAPE-TEST-001 2025-01-25 Test & verify: <invoice> with "quotes" & 'apostrophes' Smith & Jones Ltd. info@smith&jones.com Terms: 2/10 net 30 (2% if paid <= 10 days) Price comparison: USD < EUR > GBP Product "A" & Product 'B' `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const invoiceData = einvoice.getInvoiceData(); const xmlString = einvoice.getXmlString(); // Verify entities are properly escaped in output expect(xmlString).toContain('Smith & Jones Ltd.'); expect(xmlString).toContain('info@smith&jones.com'); expect(xmlString).toContain('2% if paid <= 10 days'); expect(xmlString).toContain('USD < EUR > GBP'); expect(xmlString).toContain('Product "A" & Product \'B\''); // Verify data is unescaped when accessed if (invoiceData?.notes) { expect(invoiceData.notes[0]).toContain('Test & verify: with "quotes" & \'apostrophes\''); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('basic-escaping', elapsed); }); t.test('Numeric character references', async () => { const startTime = performance.now(); // Test decimal and hexadecimal character references const xmlContent = ` 2.1 NUMERIC-REF-TEST Decimal refs: € £ ¥ ™ Hex refs: € £ ¥ ™ Mixed: © 2025 — All rights reserved™ Special chars: – — … “quoted” Math: ≤ ≥ ≠ ± ÷ × `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify numeric references are preserved or converted correctly // The implementation might convert them to actual characters or preserve as entities expect(xmlString).toMatch(/€|€|€/); // Euro expect(xmlString).toMatch(/£|£|£/); // Pound expect(xmlString).toMatch(/¥|¥|¥/); // Yen expect(xmlString).toMatch(/™|™|™/); // Trademark expect(xmlString).toMatch(/©|©/); // Copyright expect(xmlString).toMatch(/—|—|—/); // Em dash expect(xmlString).toMatch(/"|“/); // Left quote expect(xmlString).toMatch(/"|”/); // Right quote const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('numeric-refs', elapsed); }); t.test('Attribute value escaping', async () => { const startTime = performance.now(); // Test escaping in attribute values const xmlContent = ` 2.1 ATTR-ESCAPE-TEST 30 REF-2025-001 Special handling required 119.00 ITEM-001 Product with 'quotes' & "double quotes" `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Verify attributes are properly escaped expect(xmlString).toMatch(/name="Bank & Wire Transfer"|name='Bank & Wire Transfer'/); expect(xmlString).toMatch(/type="Order <123>"|type='Order <123>'/); expect(xmlString).toContain('&'); expect(xmlString).toContain('<'); expect(xmlString).toContain('>'); // Quotes in attributes should be escaped expect(xmlString).toMatch(/"|'/); // Quotes should be escaped or use different quote style const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('attribute-escaping', elapsed); }); t.test('CDATA sections with special characters', async () => { const startTime = performance.now(); // Test CDATA sections that don't need escaping const xmlContent = ` 2.1 CDATA-ESCAPE-TEST & " ' without escaping]]> Payment terms: 30 days net

]]>
SCRIPT-001 100 && currency == "EUR") { discount = amount * 0.05; } ]]> = 10 then price < 50.00]]>
`; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // CDATA content should be preserved if (xmlString.includes('CDATA')) { expect(xmlString).toContain(''); // Inside CDATA, characters are not escaped expect(xmlString).toMatch(/&].*\]\]>/); } else { // If CDATA is converted to text, it should be escaped expect(xmlString).toContain('<'); expect(xmlString).toContain('>'); expect(xmlString).toContain('&'); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('cdata-escaping', elapsed); }); t.test('Invalid character handling', async () => { const startTime = performance.now(); // Test handling of characters that are invalid in XML const xmlContent = ` 2.1 INVALID-CHAR-TEST Control chars: �     Valid controls: (tab, LF, CR) High Unicode: 𐀀 􏿿 Surrogate pairs: � � (invalid) `; const einvoice = new EInvoice(); try { await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Valid control characters should be preserved expect(xmlString).toMatch(/ | /); // Tab expect(xmlString).toMatch(/ |\n/); // Line feed expect(xmlString).toMatch(/ |\r/); // Carriage return // Invalid characters might be filtered or cause errors // Implementation specific behavior } catch (error) { // Some parsers reject invalid character references console.log('Invalid character handling:', error.message); expect(error.message).toMatch(/invalid.*character|character.*reference/i); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('invalid-chars', elapsed); }); t.test('Mixed content escaping', async () => { const startTime = performance.now(); const xmlContent = ` 2.1 MIXED-ESCAPE-TEST Regular text with & ampersand tags & ampersands]]> Payment due in < 30 days 30 false Discount for orders > €1000 50.00 `; const einvoice = new EInvoice(); await einvoice.loadFromString(xmlContent); const xmlString = einvoice.getXmlString(); // Mixed content should maintain proper escaping expect(xmlString).toContain('&'); expect(xmlString).toContain('<'); expect(xmlString).toContain('>'); const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('mixed-escaping', elapsed); }); t.test('Corpus escaping validation', async () => { const startTime = performance.now(); let processedCount = 0; let escapedCount = 0; const files = await corpusLoader.getAllFiles(); const xmlFiles = files.filter(f => f.endsWith('.xml')); // Check sample for proper escaping const sampleSize = Math.min(50, xmlFiles.length); const sample = xmlFiles.slice(0, sampleSize); for (const file of sample) { try { const content = await corpusLoader.readFile(file); const einvoice = new EInvoice(); if (typeof content === 'string') { await einvoice.loadFromString(content); } else { await einvoice.loadFromBuffer(content); } const xmlString = einvoice.getXmlString(); // Check for proper escaping if (xmlString.includes('&') || xmlString.includes('<') || xmlString.includes('>') || xmlString.includes('"') || xmlString.includes(''') || xmlString.includes('&#')) { escapedCount++; } // Verify XML is well-formed after escaping expect(xmlString).toBeTruthy(); expect(xmlString.includes(' { const startTime = performance.now(); // Test protection against XML entity expansion attacks const xmlContent = ` ]> 2.1 ENTITY-EXPANSION-TEST &lol3; `; const einvoice = new EInvoice(); try { await einvoice.loadFromString(xmlContent); // If entity expansion is allowed, check it's limited const xmlString = einvoice.getXmlString(); expect(xmlString.length).toBeLessThan(1000000); // Should not explode in size } catch (error) { // Good - entity expansion might be blocked console.log('Entity expansion protection:', error.message); expect(error.message).toMatch(/entity|expansion|security/i); } const elapsed = performance.now() - startTime; performanceTracker.addMeasurement('entity-expansion', elapsed); }); // Print performance summary performanceTracker.printSummary(); // Performance assertions const avgTime = performanceTracker.getAverageTime(); expect(avgTime).toBeLessThan(100); // Escaping operations should be fast }); tap.start();