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" ', 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 & 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 & "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(/>([^<]*)')) { 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 = ` ESCAPE-ROUNDTRIP 2025-01-25 Testing: & < > " ' 380 EUR Smith & Sons A & B Street Test City 12345 DE Customer <Test> Main St "A" Customer City 54321 DE 1 1 100.00 Item with <angle> & "quotes" `; 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 ') && itemName.includes('Item with & "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 = ` NUMERIC-REFS 2025-01-25 Numeric refs: & < > " ' 380 EUR Company & Co Test Street Test City 12345 DE Customer Customer Street Customer City 54321 DE 1 1 100.00 Test Item `; 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 = ` CDATA-TEST 2025-01-25 " ' characters]]> 380 EUR symbols]]> Test Street Test City 12345 DE Customer Customer Street Customer City 54321 DE 1 1 100.00 Test Item `; 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();