einvoice/test/suite/einvoice_edge-cases/test.edge-04.unusual-charsets.ts

564 lines
18 KiB
TypeScript
Raw Normal View History

2025-05-26 04:04:51 +00:00
import { tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../performance.tracker.js';
2025-05-27 15:26:22 +00:00
tap.test('EDGE-04: Unusual Character Sets - should handle unusual and exotic character encodings', async () => {
// Test 1: Unicode edge cases with real invoice data
await PerformanceTracker.track('unicode-edge-cases', async () => {
const testCases = [
{
name: 'zero-width-characters',
text: 'Invoice\u200B\u200C\u200D\uFEFFNumber',
description: 'Zero-width spaces and joiners'
},
{
name: 'right-to-left',
text: 'مرحبا INV-001 שלום',
description: 'RTL Arabic and Hebrew mixed with LTR'
},
{
name: 'surrogate-pairs',
text: '𝐇𝐞𝐥𝐥𝐨 😀 🎉 Invoice',
description: 'Mathematical bold text and emojis'
},
{
name: 'combining-characters',
text: 'Ińvȯíçë̃ Nüm̈bër̊',
description: 'Combining diacritical marks'
},
{
name: 'control-characters',
text: 'Invoice Test', // Remove actual control chars as they break XML
description: 'Control characters (removed for XML safety)'
},
{
name: 'bidi-override',
text: '\u202Eتسا Invoice 123\u202C',
description: 'Bidirectional override characters'
}
];
for (const testCase of testCases) {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = testCase.text;
einvoice.subject = testCase.description;
// Set required fields
einvoice.from = {
type: 'company',
name: 'Test Unicode Company',
description: testCase.description,
address: {
streetName: 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: 'Test City',
country: 'DE'
2025-05-26 04:04:51 +00:00
},
2025-05-27 15:26:22 +00:00
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
}
};
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'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Add test item
einvoice.items = [{
position: 1,
name: `Item with ${testCase.name}`,
articleNumber: 'ART-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
try {
// Export to UBL format
const ublString = await einvoice.toXmlString('ubl');
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Check if special characters are preserved
const preserved = ublString.includes(testCase.text);
console.log(`Unicode test ${testCase.name}: ${preserved ? 'preserved' : 'encoded'}`);
// Try to import it back
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(ublString);
const roundTripPreserved = newInvoice.invoiceId === testCase.text;
console.log(`Unicode test ${testCase.name} round-trip: ${roundTripPreserved ? 'success' : 'modified'}`);
} catch (error) {
console.log(`Unicode test ${testCase.name} failed: ${error.message}`);
2025-05-26 04:04:51 +00:00
}
}
});
2025-05-27 15:26:22 +00:00
// Test 2: Various character encodings in invoice content
await PerformanceTracker.track('various-character-encodings', async () => {
const encodingTests = [
{
encoding: 'UTF-8',
text: 'Übung macht den Meister - äöüß'
},
{
encoding: 'Latin',
text: 'Ñoño español - ¡Hola!'
},
{
encoding: 'Cyrillic',
text: 'Счёт-фактура № 2024'
},
{
encoding: 'Greek',
text: 'Τιμολόγιο: ΜΜΚΔ'
},
{
encoding: 'Chinese',
text: '發票編號:貳零貳肆'
}
];
for (const test of encodingTests) {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = `ENC-${test.encoding}`;
einvoice.subject = test.text;
einvoice.from = {
type: 'company',
name: test.text,
description: `Company using ${test.encoding}`,
address: {
streetName: 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: test.text,
country: 'DE'
2025-05-26 04:04:51 +00:00
},
2025-05-27 15:26:22 +00:00
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: test.text
}
};
einvoice.to = {
type: 'company',
name: 'Customer Inc',
description: 'Test customer',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
2025-05-26 04:04:51 +00:00
},
2025-05-27 15:26:22 +00:00
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Commercial Register'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.items = [{
position: 1,
name: test.text,
articleNumber: 'ART-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
try {
// Test both UBL and CII formats
for (const format of ['ubl', 'cii'] as const) {
const xmlString = await einvoice.toXmlString(format);
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Check if text is preserved
const preserved = xmlString.includes(test.text);
console.log(`Encoding test ${test.encoding} in ${format}: ${preserved ? 'preserved' : 'modified'}`);
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Import back
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(xmlString);
const descPreserved = newInvoice.subject === test.text;
console.log(`Encoding test ${test.encoding} round-trip in ${format}: ${descPreserved ? 'success' : 'failed'}`);
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
} catch (error) {
console.log(`Encoding test ${test.encoding} failed: ${error.message}`);
2025-05-26 04:04:51 +00:00
}
}
});
// Test 3: Emoji and pictographic characters
2025-05-27 15:26:22 +00:00
await PerformanceTracker.track('emoji-and-pictographs', async () => {
const emojiTests = [
{
name: 'basic-emoji',
content: 'Invoice 📧 sent ✅'
},
{
name: 'flag-emoji',
content: 'Country: 🇺🇸 🇬🇧 🇩🇪 🇫🇷'
},
{
name: 'skin-tone-emoji',
content: 'Approved by 👍🏻👍🏼👍🏽👍🏾👍🏿'
},
{
name: 'zwj-sequences',
content: 'Family: 👨‍👩‍👧‍👦'
},
{
name: 'mixed-emoji-text',
content: '💰 Total: €1,234.56 💶'
}
];
for (const test of emojiTests) {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'EMOJI-001';
einvoice.subject = test.content;
einvoice.from = {
type: 'company',
name: 'Emoji Company',
description: test.content,
address: {
streetName: 'Emoji Street',
houseNumber: '1',
postalCode: '12345',
city: 'Emoji City',
country: 'DE'
2025-05-26 04:04:51 +00:00
},
2025-05-27 15:26:22 +00:00
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.to = {
type: 'person',
name: 'Emoji',
surname: 'Customer',
salutation: 'Mr' as const,
sex: 'male' as const,
title: 'Doctor' as const,
description: 'Customer who likes emojis',
address: {
streetName: 'Customer Street',
houseNumber: '2',
postalCode: '54321',
city: 'Customer City',
country: 'DE'
}
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.items = [{
position: 1,
name: test.content,
articleNumber: 'EMOJI-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
try {
const ublString = await einvoice.toXmlString('ubl');
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Check if emoji content is preserved or encoded
const preserved = ublString.includes(test.content);
console.log(`Emoji test ${test.name}: ${preserved ? 'preserved' : 'encoded'}`);
// Count grapheme clusters (visual characters)
const graphemeCount = [...new Intl.Segmenter().segment(test.content)].length;
console.log(`Emoji test ${test.name} has ${graphemeCount} visual characters`);
} catch (error) {
console.log(`Emoji test ${test.name} failed: ${error.message}`);
2025-05-26 04:04:51 +00:00
}
}
});
// Test 4: Legacy and exotic scripts
2025-05-27 15:26:22 +00:00
await PerformanceTracker.track('exotic-scripts', async () => {
const scripts = [
{ name: 'chinese-traditional', text: '發票編號:貳零貳肆' },
{ name: 'japanese-mixed', text: '請求書番号:2024年' },
{ name: 'korean', text: '송장 번호: 2024' },
{ name: 'thai', text: 'ใบแจ้งหนี้: ๒๐๒๔' },
{ name: 'devanagari', text: 'चालान संख्या: २०२४' },
{ name: 'bengali', text: 'চালান নং: ২০২৪' },
{ name: 'tamil', text: 'விலைப்பட்டியல்: ௨௦௨௪' }
];
for (const script of scripts) {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = `SCRIPT-${script.name}`;
einvoice.subject = script.text;
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.from = {
type: 'company',
name: 'International Company',
description: script.text,
address: {
streetName: 'International Street',
houseNumber: '1',
postalCode: '12345',
city: 'International City',
country: 'DE'
},
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.to = {
type: 'company',
name: 'Local Company',
description: 'Customer',
address: {
streetName: 'Local Street',
houseNumber: '2',
postalCode: '54321',
city: 'Local City',
country: 'DE'
2025-05-26 04:04:51 +00:00
},
2025-05-27 15:26:22 +00:00
status: 'active',
foundedDate: { year: 2019, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE987654321',
registrationId: 'HRB 54321',
registrationName: 'Commercial Register'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.items = [{
position: 1,
name: script.text,
articleNumber: 'INT-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
try {
const ciiString = await einvoice.toXmlString('cii');
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
const preserved = ciiString.includes(script.text);
console.log(`Script ${script.name}: ${preserved ? 'preserved' : 'encoded'}`);
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Test round-trip
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(ciiString);
const descPreserved = newInvoice.subject === script.text;
console.log(`Script ${script.name} round-trip: ${descPreserved ? 'success' : 'modified'}`);
} catch (error) {
console.log(`Script ${script.name} failed: ${error.message}`);
2025-05-26 04:04:51 +00:00
}
}
});
2025-05-27 15:26:22 +00:00
// Test 5: XML special characters in unusual positions
await PerformanceTracker.track('xml-special-characters', async () => {
const specialChars = [
{ char: '<', desc: 'less than' },
{ char: '>', desc: 'greater than' },
{ char: '&', desc: 'ampersand' },
{ char: '"', desc: 'quote' },
{ char: "'", desc: 'apostrophe' }
];
for (const special of specialChars) {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = `XML-${special.desc}`;
einvoice.subject = `Price ${special.char} 100`;
einvoice.from = {
type: 'company',
name: `Company ${special.char} Test`,
description: 'Special char test',
address: {
streetName: 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: 'Test City',
country: 'DE'
2025-05-26 04:04:51 +00:00
},
2025-05-27 15:26:22 +00:00
status: 'active',
foundedDate: { year: 2020, month: 1, day: 1 },
registrationDetails: {
vatId: 'DE123456789',
registrationId: 'HRB 12345',
registrationName: 'Commercial Register'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
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'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.items = [{
position: 1,
name: `Item ${special.char} Test`,
articleNumber: 'SPEC-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
try {
const xmlString = await einvoice.toXmlString('ubl');
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Check if special chars are properly escaped
const escaped = xmlString.includes(`&${special.desc.replace(' ', '')};`) ||
xmlString.includes(`&#${special.char.charCodeAt(0)};`);
console.log(`XML special ${special.desc}: ${escaped ? 'properly escaped' : 'check encoding'}`);
} catch (error) {
console.log(`XML special ${special.desc} failed: ${error.message}`);
2025-05-26 04:04:51 +00:00
}
}
});
2025-05-27 15:26:22 +00:00
// Test 6: Character set conversion in format transformation
await PerformanceTracker.track('format-transform-charsets', async () => {
const testContents = [
{ name: 'multilingual', text: 'Hello مرحبا 你好 Здравствуйте' },
{ name: 'symbols', text: '€ £ ¥ $ ₹ ₽ ¢ ₩' },
{ name: 'accented', text: 'àáäâ èéëê ìíïî òóöô ùúüû ñç' },
{ name: 'mixed-emoji', text: 'Invoice 📄 Total: 💰 Status: ✅' }
];
for (const content of testContents) {
const einvoice = new EInvoice();
einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'CHARSET-001';
einvoice.subject = content.text;
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.from = {
type: 'company',
name: 'Charset Test Company',
description: content.text,
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'
2025-05-26 04:04:51 +00:00
}
2025-05-27 15:26:22 +00:00
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.to = {
type: 'company',
name: 'Customer Company',
description: '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: 'Commercial Register'
}
};
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
einvoice.items = [{
position: 1,
name: content.text,
articleNumber: 'CHARSET-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
try {
// Convert from UBL to CII
const ublString = await einvoice.toXmlString('ubl');
const newInvoice = new EInvoice();
await newInvoice.fromXmlString(ublString);
const ciiString = await newInvoice.toXmlString('cii');
2025-05-26 04:04:51 +00:00
2025-05-27 15:26:22 +00:00
// Check if content was preserved through transformation
const preserved = ciiString.includes(content.text);
console.log(`Format transform ${content.name}: ${preserved ? 'preserved' : 'modified'}`);
// Double check with round trip
const finalInvoice = new EInvoice();
await finalInvoice.fromXmlString(ciiString);
const roundTripPreserved = finalInvoice.subject === content.text;
console.log(`Format transform ${content.name} round-trip: ${roundTripPreserved ? 'success' : 'failed'}`);
} catch (error) {
console.log(`Format transform ${content.name} failed: ${error.message}`);
2025-05-26 04:04:51 +00:00
}
}
});
});
// Run the test
tap.start();