einvoice/test/suite/einvoice_encoding/test.enc-05.special-characters.ts

403 lines
15 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
tap.test('ENC-05: Special Characters - should handle special XML characters correctly', async () => {
console.log('Testing special character handling in XML content...\n');
// Test 1: Unicode special characters
const testUnicodeSpecialChars = async () => {
const einvoice = new EInvoice();
einvoice.id = 'UNICODE-SPECIAL-TEST';
einvoice.date = Date.now();
einvoice.currency = 'EUR';
// Test various special Unicode characters
const specialChars = {
mathematical: '∑∏∆∇∂∞≠≤≥±∓×÷√∝∴∵∠∟⊥∥∦',
currency: '€£¥₹₽₩₪₨₫₡₢₣₤₥₦₧₨₩₪₫',
symbols: '™®©℗℠⁋⁌⁍⁎⁏⁐⁑⁒⁓⁔⁕⁖⁗⁘⁙⁚⁛⁜⁝⁞',
arrows: '←→↑↓↔↕↖↗↘↙↚↛↜↝↞↟↠↡↢↣↤↥',
punctuation: '‚„"«»‹›§¶†‡•‰‱′″‴‵‶‷‸‼⁇⁈⁉⁊⁋⁌⁍⁎⁏'
};
einvoice.subject = `Unicode test: ${specialChars.mathematical.substring(0, 10)}`;
einvoice.notes = [
`Math: ${specialChars.mathematical}`,
`Currency: ${specialChars.currency}`,
`Symbols: ${specialChars.symbols}`,
`Arrows: ${specialChars.arrows}`,
`Punctuation: ${specialChars.punctuation}`
];
einvoice.from = {
type: 'company',
name: 'Special Characters Inc ™',
description: 'Company with special symbols: ®©',
address: {
streetName: 'Unicode Street ←→',
houseNumber: '∞',
postalCode: '12345',
city: 'Symbol City ≤≥',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Special Registry ™'
}
};
einvoice.to = {
type: 'company',
name: 'Customer Ltd ©',
description: 'Customer with currency: €£¥',
address: {
streetName: 'Currency Ave',
houseNumber: '€1',
postalCode: '54321',
city: 'Money City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Customer Registry'
}
};
einvoice.items = [{
position: 1,
name: 'Product with symbols: ∑∏∆',
unitType: 'C62',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('ubl');
// Check if special characters are preserved or properly encoded
const mathPreserved = specialChars.mathematical.split('').filter(char =>
xmlString.includes(char) ||
xmlString.includes(`&#${char.charCodeAt(0)};`) ||
xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`)
).length;
const currencyPreserved = specialChars.currency.split('').filter(char =>
xmlString.includes(char) ||
xmlString.includes(`&#${char.charCodeAt(0)};`) ||
xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`)
).length;
const symbolsPreserved = specialChars.symbols.split('').filter(char =>
xmlString.includes(char) ||
xmlString.includes(`&#${char.charCodeAt(0)};`) ||
xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`)
).length;
return {
mathPreserved,
currencyPreserved,
symbolsPreserved,
totalMath: specialChars.mathematical.length,
totalCurrency: specialChars.currency.length,
totalSymbols: specialChars.symbols.length,
xmlString
};
};
const unicodeResult = await testUnicodeSpecialChars();
console.log('Test 1 - Unicode special characters:');
console.log(` Mathematical symbols: ${unicodeResult.mathPreserved}/${unicodeResult.totalMath} preserved`);
console.log(` Currency symbols: ${unicodeResult.currencyPreserved}/${unicodeResult.totalCurrency} preserved`);
console.log(` Other symbols: ${unicodeResult.symbolsPreserved}/${unicodeResult.totalSymbols} preserved`);
// Test 2: Control characters and whitespace
const testControlCharacters = async () => {
const einvoice = new EInvoice();
einvoice.id = 'CONTROL-CHARS-TEST';
einvoice.date = Date.now();
einvoice.currency = 'EUR';
// Test various whitespace and control characters
einvoice.subject = 'Control chars test:\ttab\nnewline\rcarriage return';
einvoice.notes = [
'Tab separated:\tvalue1\tvalue2\tvalue3',
'Line break:\nSecond line\nThird line',
'Mixed whitespace: spaces \t tabs \r\n mixed'
];
einvoice.from = {
type: 'company',
name: 'Control\tCharacters\nCompany',
description: 'Company\twith\ncontrol\rcharacters',
address: {
streetName: 'Control 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: 'Registry'
}
};
einvoice.to = {
type: 'company',
name: 'Customer',
description: 'Normal customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Customer Registry'
}
};
einvoice.items = [{
position: 1,
name: 'Product\twith\ncontrol\rchars',
unitType: 'C62',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('ubl');
// Check how control characters are handled
const hasTabHandling = xmlString.includes('	') || xmlString.includes('	') ||
xmlString.includes('\t') || !xmlString.includes('Control\tCharacters');
const hasNewlineHandling = xmlString.includes('
') || xmlString.includes('
') ||
xmlString.includes('\n') || !xmlString.includes('Characters\nCompany');
const hasCarriageReturnHandling = xmlString.includes('
') || xmlString.includes('
') ||
xmlString.includes('\r') || !xmlString.includes('control\rcharacters');
return {
hasTabHandling,
hasNewlineHandling,
hasCarriageReturnHandling,
xmlString
};
};
const controlResult = await testControlCharacters();
console.log('\nTest 2 - Control characters and whitespace:');
console.log(` Tab handling: ${controlResult.hasTabHandling ? 'Yes' : 'No'}`);
console.log(` Newline handling: ${controlResult.hasNewlineHandling ? 'Yes' : 'No'}`);
console.log(` Carriage return handling: ${controlResult.hasCarriageReturnHandling ? 'Yes' : 'No'}`);
// Test 3: Emojis and extended Unicode
const testEmojisAndExtended = async () => {
const einvoice = new EInvoice();
einvoice.id = 'EMOJI-TEST';
einvoice.date = Date.now();
einvoice.currency = 'EUR';
// Test emojis and extended Unicode
const emojis = '😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗☺😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁☹😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠💩🤡👹👺👻👽👾🤖😺😸😹😻😼😽🙀😿😾🙈🙉🙊💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤';
einvoice.subject = `Emoji test: ${emojis.substring(0, 20)}`;
einvoice.notes = [
`Faces: ${emojis.substring(0, 50)}`,
`Hearts: 💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍`,
`Objects: 💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤`
];
einvoice.from = {
type: 'company',
name: 'Emoji Company 😊',
description: 'Company with emojis 🏢',
address: {
streetName: 'Happy Street 😃',
houseNumber: '1⃣',
postalCode: '12345',
city: 'Emoji City 🌆',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Registry 📝'
}
};
einvoice.to = {
type: 'company',
name: 'Customer 🛍️',
description: 'Shopping customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Customer Registry'
}
};
einvoice.items = [{
position: 1,
name: 'Emoji Product 📦',
unitType: 'C62',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
const xmlString = await einvoice.toXmlString('ubl');
// Check if emojis are preserved or encoded
const emojiCount = emojis.split('').filter(char => {
const codePoint = char.codePointAt(0);
return codePoint && codePoint > 0xFFFF; // Emojis are typically above the BMP
}).length;
const preservedEmojis = emojis.split('').filter(char => {
const codePoint = char.codePointAt(0);
if (!codePoint || codePoint <= 0xFFFF) return false;
return xmlString.includes(char) ||
xmlString.includes(`&#${codePoint};`) ||
xmlString.includes(`&#x${codePoint.toString(16)};`);
}).length;
return {
emojiCount,
preservedEmojis,
preservationRate: emojiCount > 0 ? (preservedEmojis / emojiCount) * 100 : 0
};
};
const emojiResult = await testEmojisAndExtended();
console.log('\nTest 3 - Emojis and extended Unicode:');
console.log(` Emoji preservation: ${emojiResult.preservedEmojis}/${emojiResult.emojiCount} (${emojiResult.preservationRate.toFixed(1)}%)`);
// Test 4: XML predefined entities in content
const testXmlPredefinedEntities = async () => {
const xmlWithEntities = `<?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>ENTITIES-TEST</cbc:ID>
<cbc:IssueDate>2025-01-25</cbc:IssueDate>
<cbc:Note>Entities: &amp; &lt; &gt; &quot; &apos;</cbc:Note>
<cbc:InvoiceTypeCode>380</cbc:InvoiceTypeCode>
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
<cac:AccountingSupplierParty>
<cac:Party>
<cac:PartyName>
<cbc:Name>Entity &amp; Company</cbc:Name>
</cac:PartyName>
<cac:PostalAddress>
<cbc:StreetName>&lt;Special&gt; Street</cbc:StreetName>
<cbc:CityName>Entity 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 &quot;Quotes&quot;</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>Product &apos;Apostrophe&apos;</cbc:Name>
</cac:Item>
</cac:InvoiceLine>
</Invoice>`;
try {
const invoice = await EInvoice.fromXml(xmlWithEntities);
const supplierName = invoice.from?.name || '';
const customerName = invoice.to?.name || '';
const itemName = invoice.items?.[0]?.name || '';
const entitiesDecoded =
supplierName.includes('Entity & Company') &&
customerName.includes('Customer "Quotes"') &&
itemName.includes("Product 'Apostrophe'");
return {
success: invoice.id === 'ENTITIES-TEST',
entitiesDecoded,
supplierName,
customerName,
itemName
};
} catch (error) {
return {
success: false,
error: error.message
};
}
};
const entitiesResult = await testXmlPredefinedEntities();
console.log('\nTest 4 - XML predefined entities:');
console.log(` Invoice parsed: ${entitiesResult.success ? 'Yes' : 'No'}`);
console.log(` Entities decoded: ${entitiesResult.entitiesDecoded ? 'Yes' : 'No'}`);
if (entitiesResult.error) {
console.log(` Error: ${entitiesResult.error}`);
}
// Summary
console.log('\n=== Special Characters Test Summary ===');
const unicodeScore = (unicodeResult.mathPreserved + unicodeResult.currencyPreserved + unicodeResult.symbolsPreserved) /
(unicodeResult.totalMath + unicodeResult.totalCurrency + unicodeResult.totalSymbols) * 100;
console.log(`Unicode symbols: ${unicodeScore.toFixed(1)}% preserved`);
console.log(`Control characters: ${controlResult.hasTabHandling && controlResult.hasNewlineHandling ? 'Handled' : 'Issues'}`);
console.log(`Emojis: ${emojiResult.preservationRate.toFixed(1)}% preserved`);
console.log(`XML entities: ${entitiesResult.success && entitiesResult.entitiesDecoded ? 'Working' : 'Issues'}`);
// Tests pass if basic functionality works
expect(unicodeScore).toBeGreaterThan(50); // At least 50% of Unicode symbols preserved
expect(entitiesResult.success).toEqual(true);
expect(entitiesResult.entitiesDecoded).toEqual(true);
console.log('\n✓ Special characters test completed');
});
tap.start();