488 lines
16 KiB
TypeScript
488 lines
16 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
||
import { EInvoice } from '../../../ts/index.js';
|
||
|
||
tap.test('EDGE-04: Unusual Character Sets - should handle unusual and exotic character encodings', async () => {
|
||
console.log('Testing unusual character sets in e-invoices...\n');
|
||
|
||
// Test 1: Unicode edge cases with real invoice data
|
||
const testUnicodeEdgeCases = 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'
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const testCase of testCases) {
|
||
try {
|
||
const einvoice = new EInvoice();
|
||
einvoice.issueDate = new Date(2024, 0, 1);
|
||
einvoice.id = testCase.text;
|
||
einvoice.subject = testCase.description;
|
||
|
||
// Set required fields for EN16931 compliance
|
||
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
|
||
}];
|
||
|
||
// Export to UBL format
|
||
const ublString = await einvoice.toXmlString('ubl');
|
||
|
||
// Check if special characters are preserved
|
||
const preserved = ublString.includes(testCase.text);
|
||
|
||
// Try to import it back
|
||
const newInvoice = new EInvoice();
|
||
await newInvoice.fromXmlString(ublString);
|
||
|
||
const roundTripPreserved = (newInvoice.id === testCase.text ||
|
||
newInvoice.invoiceId === testCase.text ||
|
||
newInvoice.accountingDocId === testCase.text);
|
||
|
||
console.log(`Test 1.${testCase.name}:`);
|
||
console.log(` Unicode preserved in XML: ${preserved ? 'Yes' : 'No'}`);
|
||
console.log(` Round-trip successful: ${roundTripPreserved ? 'Yes' : 'No'}`);
|
||
|
||
results.push({ name: testCase.name, preserved, roundTripPreserved });
|
||
} catch (error) {
|
||
console.log(`Test 1.${testCase.name}:`);
|
||
console.log(` Error: ${error.message}`);
|
||
results.push({ name: testCase.name, preserved: false, roundTripPreserved: false, error: error.message });
|
||
}
|
||
}
|
||
|
||
return results;
|
||
};
|
||
|
||
// Test 2: Various character encodings in invoice content
|
||
const testVariousEncodings = 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: '發票編號:貳零貳肆'
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of encodingTests) {
|
||
try {
|
||
const einvoice = new EInvoice();
|
||
einvoice.issueDate = new Date(2024, 0, 1);
|
||
einvoice.id = `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 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: test.text,
|
||
articleNumber: 'ENC-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
// Test both UBL and CII formats
|
||
const ublString = await einvoice.toXmlString('ubl');
|
||
const ciiString = await einvoice.toXmlString('cii');
|
||
|
||
// Check preservation in both formats
|
||
const ublPreserved = ublString.includes(test.text);
|
||
const ciiPreserved = ciiString.includes(test.text);
|
||
|
||
// Test round-trip for both formats
|
||
const ublInvoice = new EInvoice();
|
||
await ublInvoice.fromXmlString(ublString);
|
||
|
||
const ciiInvoice = new EInvoice();
|
||
await ciiInvoice.fromXmlString(ciiString);
|
||
|
||
const ublRoundTrip = ublInvoice.from?.name?.includes(test.text.substring(0, 10)) || false;
|
||
const ciiRoundTrip = ciiInvoice.from?.name?.includes(test.text.substring(0, 10)) || false;
|
||
|
||
console.log(`\nTest 2.${test.encoding}:`);
|
||
console.log(` UBL preserves encoding: ${ublPreserved ? 'Yes' : 'No'}`);
|
||
console.log(` CII preserves encoding: ${ciiPreserved ? 'Yes' : 'No'}`);
|
||
console.log(` UBL round-trip: ${ublRoundTrip ? 'Yes' : 'No'}`);
|
||
console.log(` CII round-trip: ${ciiRoundTrip ? 'Yes' : 'No'}`);
|
||
|
||
results.push({
|
||
encoding: test.encoding,
|
||
ublPreserved,
|
||
ciiPreserved,
|
||
ublRoundTrip,
|
||
ciiRoundTrip
|
||
});
|
||
} catch (error) {
|
||
console.log(`\nTest 2.${test.encoding}:`);
|
||
console.log(` Error: ${error.message}`);
|
||
results.push({
|
||
encoding: test.encoding,
|
||
ublPreserved: false,
|
||
ciiPreserved: false,
|
||
ublRoundTrip: false,
|
||
ciiRoundTrip: false,
|
||
error: error.message
|
||
});
|
||
}
|
||
}
|
||
|
||
return results;
|
||
};
|
||
|
||
// Test 3: Extremely unusual characters
|
||
const testExtremelyUnusualChars = async () => {
|
||
const extremeTests = [
|
||
{
|
||
name: 'ancient-scripts',
|
||
text: '𐀀𐀁𐀂 Invoice 𓀀𓀁𓀂',
|
||
description: 'Linear B and Egyptian hieroglyphs'
|
||
},
|
||
{
|
||
name: 'musical-symbols',
|
||
text: '♪♫♪ Invoice ♫♪♫',
|
||
description: 'Musical notation symbols'
|
||
},
|
||
{
|
||
name: 'math-symbols',
|
||
text: '∫∂ Invoice ∆∇',
|
||
description: 'Mathematical operators'
|
||
},
|
||
{
|
||
name: 'private-use',
|
||
text: '\uE000\uE001 Invoice \uE002\uE003',
|
||
description: 'Private use area characters'
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of extremeTests) {
|
||
try {
|
||
const einvoice = new EInvoice();
|
||
einvoice.id = `EXTREME-${test.name}`;
|
||
einvoice.issueDate = new Date(2024, 0, 1);
|
||
einvoice.subject = test.description;
|
||
|
||
einvoice.from = {
|
||
type: 'company',
|
||
name: `Company ${test.text}`,
|
||
description: test.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'
|
||
}
|
||
};
|
||
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: `Product ${test.text}`,
|
||
articleNumber: 'EXT-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
const xmlString = await einvoice.toXmlString('ubl');
|
||
const preserved = xmlString.includes(test.text);
|
||
|
||
const newInvoice = new EInvoice();
|
||
await newInvoice.fromXmlString(xmlString);
|
||
|
||
const roundTrip = newInvoice.from?.name?.includes(test.text) || false;
|
||
|
||
console.log(`\nTest 3.${test.name}:`);
|
||
console.log(` Extreme chars preserved: ${preserved ? 'Yes' : 'No'}`);
|
||
console.log(` Round-trip successful: ${roundTrip ? 'Yes' : 'No'}`);
|
||
|
||
results.push({ name: test.name, preserved, roundTrip });
|
||
} catch (error) {
|
||
console.log(`\nTest 3.${test.name}:`);
|
||
console.log(` Error: ${error.message}`);
|
||
results.push({ name: test.name, preserved: false, roundTrip: false, error: error.message });
|
||
}
|
||
}
|
||
|
||
return results;
|
||
};
|
||
|
||
// Test 4: Normalization issues
|
||
const testNormalizationIssues = async () => {
|
||
const normalizationTests = [
|
||
{
|
||
name: 'nfc-nfd',
|
||
nfc: 'é', // NFC: single character
|
||
nfd: 'é', // NFD: e + combining acute
|
||
description: 'NFC vs NFD normalization'
|
||
},
|
||
{
|
||
name: 'ligatures',
|
||
text: 'ff Invoice ffi', // ff and ffi ligatures
|
||
description: 'Unicode ligatures'
|
||
}
|
||
];
|
||
|
||
const results = [];
|
||
|
||
for (const test of normalizationTests) {
|
||
try {
|
||
const einvoice = new EInvoice();
|
||
einvoice.id = `NORM-${test.name}`;
|
||
einvoice.issueDate = new Date(2024, 0, 1);
|
||
einvoice.subject = test.description;
|
||
|
||
// Use the test text in company name
|
||
const testText = test.text || test.nfc;
|
||
|
||
einvoice.from = {
|
||
type: 'company',
|
||
name: `Company ${testText}`,
|
||
description: test.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'
|
||
}
|
||
};
|
||
|
||
einvoice.items = [{
|
||
position: 1,
|
||
name: `Product ${testText}`,
|
||
articleNumber: 'NORM-001',
|
||
unitType: 'EA',
|
||
unitQuantity: 1,
|
||
unitNetPrice: 100,
|
||
vatPercentage: 19
|
||
}];
|
||
|
||
const xmlString = await einvoice.toXmlString('ubl');
|
||
const preserved = xmlString.includes(testText);
|
||
|
||
const newInvoice = new EInvoice();
|
||
await newInvoice.fromXmlString(xmlString);
|
||
|
||
const roundTrip = newInvoice.from?.name?.includes(testText) || false;
|
||
|
||
console.log(`\nTest 4.${test.name}:`);
|
||
console.log(` Normalization preserved: ${preserved ? 'Yes' : 'No'}`);
|
||
console.log(` Round-trip successful: ${roundTrip ? 'Yes' : 'No'}`);
|
||
|
||
results.push({ name: test.name, preserved, roundTrip });
|
||
} catch (error) {
|
||
console.log(`\nTest 4.${test.name}:`);
|
||
console.log(` Error: ${error.message}`);
|
||
results.push({ name: test.name, preserved: false, roundTrip: false, error: error.message });
|
||
}
|
||
}
|
||
|
||
return results;
|
||
};
|
||
|
||
// Run all tests
|
||
const unicodeResults = await testUnicodeEdgeCases();
|
||
const encodingResults = await testVariousEncodings();
|
||
const extremeResults = await testExtremelyUnusualChars();
|
||
const normalizationResults = await testNormalizationIssues();
|
||
|
||
console.log(`\n=== Unusual Character Sets Test Summary ===`);
|
||
|
||
// Count successful tests
|
||
const unicodeSuccess = unicodeResults.filter(r => r.roundTripPreserved).length;
|
||
const encodingSuccess = encodingResults.filter(r => r.ublRoundTrip || r.ciiRoundTrip).length;
|
||
const extremeSuccess = extremeResults.filter(r => r.roundTrip).length;
|
||
const normalizationSuccess = normalizationResults.filter(r => r.roundTrip).length;
|
||
|
||
console.log(`Unicode edge cases: ${unicodeSuccess}/${unicodeResults.length} successful`);
|
||
console.log(`Various encodings: ${encodingSuccess}/${encodingResults.length} successful`);
|
||
console.log(`Extreme characters: ${extremeSuccess}/${extremeResults.length} successful`);
|
||
console.log(`Normalization tests: ${normalizationSuccess}/${normalizationResults.length} successful`);
|
||
|
||
// Test passes if at least basic Unicode handling works
|
||
const basicUnicodeWorks = unicodeResults.some(r => r.roundTripPreserved);
|
||
const basicEncodingWorks = encodingResults.some(r => r.ublRoundTrip || r.ciiRoundTrip);
|
||
|
||
expect(basicUnicodeWorks).toBeTrue();
|
||
expect(basicEncodingWorks).toBeTrue();
|
||
});
|
||
|
||
// Run the test
|
||
tap.start(); |