564 lines
18 KiB
TypeScript
564 lines
18 KiB
TypeScript
import { tap } from '@git.zone/tstest/tapbundle';
|
||
import { EInvoice } from '../../../ts/index.js';
|
||
import { PerformanceTracker } from '../performance.tracker.js';
|
||
|
||
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'
|
||
},
|
||
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'
|
||
}
|
||
};
|
||
|
||
// Add test item
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: `Item with ${testCase.name}`,
|
||
articleNumber: 'ART-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
try {
|
||
// Export to UBL format
|
||
const ublString = await einvoice.toXmlString('ubl');
|
||
|
||
// 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}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 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'
|
||
},
|
||
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'
|
||
},
|
||
status: 'active',
|
||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||
registrationDetails: {
|
||
vatId: 'DE987654321',
|
||
registrationId: 'HRB 54321',
|
||
registrationName: 'Commercial Register'
|
||
}
|
||
};
|
||
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: test.text,
|
||
articleNumber: 'ART-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
try {
|
||
// Test both UBL and CII formats
|
||
for (const format of ['ubl', 'cii'] as const) {
|
||
const xmlString = await einvoice.toXmlString(format);
|
||
|
||
// Check if text is preserved
|
||
const preserved = xmlString.includes(test.text);
|
||
console.log(`Encoding test ${test.encoding} in ${format}: ${preserved ? 'preserved' : 'modified'}`);
|
||
|
||
// 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'}`);
|
||
}
|
||
} catch (error) {
|
||
console.log(`Encoding test ${test.encoding} failed: ${error.message}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Test 3: Emoji and pictographic characters
|
||
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'
|
||
},
|
||
status: 'active',
|
||
foundedDate: { year: 2020, month: 1, day: 1 },
|
||
registrationDetails: {
|
||
vatId: 'DE123456789',
|
||
registrationId: 'HRB 12345',
|
||
registrationName: 'Commercial Register'
|
||
}
|
||
};
|
||
|
||
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'
|
||
}
|
||
};
|
||
|
||
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');
|
||
|
||
// 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}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// Test 4: Legacy and exotic scripts
|
||
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;
|
||
|
||
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'
|
||
}
|
||
};
|
||
|
||
einvoice.to = {
|
||
type: 'company',
|
||
name: 'Local Company',
|
||
description: 'Customer',
|
||
address: {
|
||
streetName: 'Local Street',
|
||
houseNumber: '2',
|
||
postalCode: '54321',
|
||
city: 'Local City',
|
||
country: 'DE'
|
||
},
|
||
status: 'active',
|
||
foundedDate: { year: 2019, month: 1, day: 1 },
|
||
registrationDetails: {
|
||
vatId: 'DE987654321',
|
||
registrationId: 'HRB 54321',
|
||
registrationName: 'Commercial Register'
|
||
}
|
||
};
|
||
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: script.text,
|
||
articleNumber: 'INT-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
try {
|
||
const ciiString = await einvoice.toXmlString('cii');
|
||
|
||
const preserved = ciiString.includes(script.text);
|
||
console.log(`Script ${script.name}: ${preserved ? 'preserved' : 'encoded'}`);
|
||
|
||
// 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}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 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'
|
||
},
|
||
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'
|
||
}
|
||
};
|
||
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: `Item ${special.char} Test`,
|
||
articleNumber: 'SPEC-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
try {
|
||
const xmlString = await einvoice.toXmlString('ubl');
|
||
|
||
// 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}`);
|
||
}
|
||
}
|
||
});
|
||
|
||
// 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;
|
||
|
||
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'
|
||
}
|
||
};
|
||
|
||
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'
|
||
}
|
||
};
|
||
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: content.text,
|
||
articleNumber: 'CHARSET-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
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');
|
||
|
||
// 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}`);
|
||
}
|
||
}
|
||
});
|
||
});
|
||
|
||
// Run the test
|
||
tap.start(); |