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();