diff --git a/package.json b/package.json
index 894311c..ad957e0 100644
--- a/package.json
+++ b/package.json
@@ -17,8 +17,8 @@
"@git.zone/tsbuild": "^2.6.4",
"@git.zone/tsbundle": "^2.2.5",
"@git.zone/tsrun": "^1.3.3",
- "@git.zone/tstest": "^2.2.5",
- "@types/node": "^22.15.21"
+ "@git.zone/tstest": "^2.3.1",
+ "@types/node": "^22.15.23"
},
"dependencies": {
"@push.rocks/smartfile": "^11.2.5",
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index c2b405a..5a1abb4 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -43,11 +43,11 @@ importers:
specifier: ^1.3.3
version: 1.3.3
'@git.zone/tstest':
- specifier: ^2.2.5
- version: 2.2.5(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)(typescript@5.8.3)
+ specifier: ^2.3.1
+ version: 2.3.1(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)(typescript@5.8.3)
'@types/node':
- specifier: ^22.15.21
- version: 22.15.21
+ specifier: ^22.15.23
+ version: 22.15.23
packages:
@@ -244,8 +244,8 @@ packages:
resolution: {integrity: sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==}
engines: {node: '>=6.9.0'}
- '@cloudflare/workers-types@4.20250525.0':
- resolution: {integrity: sha512-3loeNVJkcDLb9giarUIHmDgvh+/4RtH+R/rHn4BCmME1qKdu73n/hvECYhH8BabCZplF8zQy1wok1MKwXEWC/A==}
+ '@cloudflare/workers-types@4.20250528.0':
+ resolution: {integrity: sha512-q4yNRSw7At9nm1GsN+KUGfbrl5nGuiS+q/1esbhcXL/FQUDUZbVdutbPBMtJLaXAd5PCLdOST/nZni8GzJkaYg==}
'@colors/colors@1.6.0':
resolution: {integrity: sha512-Ir+AOibqzrIsL6ajt3Rz3LskB7OiMVHqltZmspbW/TJuTVuyOMirVqAkjfY6JISiLHgyNqicAC8AyHHGzNd/dA==}
@@ -604,8 +604,8 @@ packages:
resolution: {integrity: sha512-DDzWunkxXLtXJTxBf4EioXLwhuqdA2VzdTmOzWrw4Z4Qnms/YM67q36yajwNohAajPYyRz5DayU0ikrceFXyVw==}
hasBin: true
- '@git.zone/tstest@2.2.5':
- resolution: {integrity: sha512-KLj32yIznLIFMX6U9eEumEKI7NLNYpEHeGzD/BfqF+GvfVL8eVmdmI3GR6Cdj013C9F9nQBKnpDG5eDJnxBZEA==}
+ '@git.zone/tstest@2.3.1':
+ resolution: {integrity: sha512-VrgVhh3xJFIuBd0nRyujrXvCMaPZokGzbGesOCLDs4Qs4cGvUkf6WVMwKT5A73fn6YPZK79iTp9OqBHdV67OPw==}
hasBin: true
'@happy-dom/global-registrator@15.11.7':
@@ -1358,8 +1358,8 @@ packages:
'@types/node-forge@1.3.11':
resolution: {integrity: sha512-FQx220y22OKNTqaByeBGqHWYz4cl94tpcxeFdvBo3wjG6XPBuZ0BNgNZRV5J5TFmmcsJ4IzsLkmGRiQbnYsBEQ==}
- '@types/node@22.15.21':
- resolution: {integrity: sha512-EV/37Td6c+MgKAbkcLG6vqZ2zEYHD7bvSrzqqs2RIhbA6w3x+Dqz8MZM3sP6kGTeLrdoOgKZe+Xja7tUB2DNkQ==}
+ '@types/node@22.15.23':
+ resolution: {integrity: sha512-7Ec1zaFPF4RJ0eXu1YT/xgiebqwqoJz8rYPDi/O2BcZ++Wpt0Kq9cl0eg6NN6bYbPnR67ZLo7St5Q3UK0SnARw==}
'@types/ping@0.4.4':
resolution: {integrity: sha512-ifvo6w2f5eJYlXm+HiVx67iJe8WZp87sfa683nlqED5Vnt9Z93onkokNoWqOG21EaE8fMxyKPobE+mkPEyxsdw==}
@@ -3859,8 +3859,8 @@ packages:
resolution: {integrity: sha512-2OQsPNEmBCvXuFlIni/a+Rn+R2pHW9INm0BxXJ4hVDA8TirqMj+J/Rp9ItLatT/5pZqWwefVrTQcHpixsxnVlA==}
engines: {node: '>= 4.0.0'}
- zod@3.25.28:
- resolution: {integrity: sha512-/nt/67WYKnr5by3YS7LroZJbtcCBurDKKPBPWWzaxvVCGuG/NOsiKkrjoOhI8mJ+SQUXEbUzeB3S+6XDUEEj7Q==}
+ zod@3.25.32:
+ resolution: {integrity: sha512-OSm2xTIRfW8CV5/QKgngwmQW/8aPfGdaQFlrGoErlgg/Epm7cjb6K6VEyExfe65a3VybUOnu381edLb0dfJl0g==}
zwitch@2.0.4:
resolution: {integrity: sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==}
@@ -3888,7 +3888,7 @@ snapshots:
'@api.global/typedrequest': 3.1.10
'@api.global/typedrequest-interfaces': 3.0.19
'@api.global/typedsocket': 3.0.1
- '@cloudflare/workers-types': 4.20250525.0
+ '@cloudflare/workers-types': 4.20250528.0
'@design.estate/dees-comms': 1.0.27
'@push.rocks/lik': 6.2.2
'@push.rocks/smartchok': 1.0.34
@@ -4508,7 +4508,7 @@ snapshots:
dependencies:
regenerator-runtime: 0.14.1
- '@cloudflare/workers-types@4.20250525.0': {}
+ '@cloudflare/workers-types@4.20250528.0': {}
'@colors/colors@1.6.0': {}
@@ -4785,7 +4785,7 @@ snapshots:
'@push.rocks/smartshell': 3.2.2
tsx: 4.19.2
- '@git.zone/tstest@2.2.5(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)(typescript@5.8.3)':
+ '@git.zone/tstest@2.3.1(@aws-sdk/credential-providers@3.817.0)(socks@2.8.4)(typescript@5.8.3)':
dependencies:
'@api.global/typedserver': 3.0.74
'@git.zone/tsbundle': 2.2.5
@@ -6103,22 +6103,22 @@ snapshots:
'@types/body-parser@1.19.5':
dependencies:
'@types/connect': 3.4.38
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/buffer-json@2.0.3': {}
'@types/clean-css@4.2.11':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
source-map: 0.6.1
'@types/connect@3.4.38':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/cors@2.8.18':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/debug@4.1.12':
dependencies:
@@ -6128,7 +6128,7 @@ snapshots:
'@types/express-serve-static-core@5.0.6':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/qs': 6.14.0
'@types/range-parser': 1.2.7
'@types/send': 0.17.4
@@ -6145,30 +6145,30 @@ snapshots:
'@types/from2@2.3.5':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/fs-extra@11.0.4':
dependencies:
'@types/jsonfile': 6.1.4
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/fs-extra@9.0.13':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/glob@7.2.0':
dependencies:
'@types/minimatch': 5.1.2
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/glob@8.1.0':
dependencies:
'@types/minimatch': 5.1.2
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/gunzip-maybe@1.4.2':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/hast@3.0.4':
dependencies:
@@ -6190,7 +6190,7 @@ snapshots:
'@types/jsonfile@6.1.4':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/mdast@4.0.4':
dependencies:
@@ -6208,9 +6208,9 @@ snapshots:
'@types/node-forge@1.3.11':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
- '@types/node@22.15.21':
+ '@types/node@22.15.23':
dependencies:
undici-types: 6.21.0
@@ -6226,30 +6226,30 @@ snapshots:
'@types/s3rver@3.7.4':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/semver@7.7.0': {}
'@types/send@0.17.4':
dependencies:
'@types/mime': 1.3.5
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/serve-static@1.15.7':
dependencies:
'@types/http-errors': 2.0.4
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/send': 0.17.4
'@types/symbol-tree@3.2.5': {}
'@types/tar-stream@2.2.3':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/through2@2.0.41':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/triple-beam@1.3.5': {}
@@ -6273,18 +6273,18 @@ snapshots:
'@types/whatwg-url@8.2.2':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/webidl-conversions': 7.0.3
'@types/which@3.0.4': {}
'@types/ws@8.18.1':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
'@types/yauzl@2.10.3':
dependencies:
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
optional: true
'@ungap/structured-clone@1.3.0': {}
@@ -6508,7 +6508,7 @@ snapshots:
dependencies:
devtools-protocol: 0.0.1439962
mitt: 3.0.1
- zod: 3.25.28
+ zod: 3.25.32
clean-css@4.2.4:
dependencies:
@@ -6769,7 +6769,7 @@ snapshots:
engine.io@6.6.4:
dependencies:
'@types/cors': 2.8.18
- '@types/node': 22.15.21
+ '@types/node': 22.15.23
accepts: 1.3.8
base64id: 2.0.0
cookie: 0.7.2
@@ -9090,6 +9090,6 @@ snapshots:
ylru@1.4.0: {}
- zod@3.25.28: {}
+ zod@3.25.32: {}
zwitch@2.0.4: {}
diff --git a/test/suite/einvoice_edge-cases/test.edge-04.unusual-charsets.ts b/test/suite/einvoice_edge-cases/test.edge-04.unusual-charsets.ts
index 03a7a9a..8a4563c 100644
--- a/test/suite/einvoice_edge-cases/test.edge-04.unusual-charsets.ts
+++ b/test/suite/einvoice_edge-cases/test.edge-04.unusual-charsets.ts
@@ -1,10 +1,11 @@
-import { tap } from '@git.zone/tstest/tapbundle';
+import { expect, 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 () => {
+ console.log('Testing unusual character sets in e-invoices...\n');
+
// Test 1: Unicode edge cases with real invoice data
- await PerformanceTracker.track('unicode-edge-cases', async () => {
+ const testUnicodeEdgeCases = async () => {
const testCases = [
{
name: 'zero-width-characters',
@@ -38,83 +39,95 @@ tap.test('EDGE-04: Unusual Character Sets - should handle unusual and exotic cha
}
];
+ const results = [];
+
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 {
+ 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);
- 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'}`);
+ 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(`Unicode test ${testCase.name} failed: ${error.message}`);
+ 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
- await PerformanceTracker.track('various-character-encodings', async () => {
+ const testVariousEncodings = async () => {
const encodingTests = [
{
encoding: 'UTF-8',
@@ -138,426 +151,337 @@ tap.test('EDGE-04: Unusual Character Sets - should handle unusual and exotic cha
}
];
+ const results = [];
+
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 {
+ 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
- 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'}`);
+ // Check preservation in both formats
+ const ublPreserved = ublString.includes(test.text);
+ const ciiPreserved = ciiString.includes(test.text);
- // Test round-trip
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(ciiString);
+ // Test round-trip for both formats
+ const ublInvoice = new EInvoice();
+ await ublInvoice.fromXmlString(ublString);
- const descPreserved = newInvoice.subject === script.text;
- console.log(`Script ${script.name} round-trip: ${descPreserved ? 'success' : 'modified'}`);
+ 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(`Script ${script.name} failed: ${error.message}`);
+ 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 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' }
+ // 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'
+ }
];
- 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
- }];
-
+ 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);
- // 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'}`);
+ 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(`XML special ${special.desc} failed: ${error.message}`);
+ 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 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: ✅' }
+ // 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'
+ }
];
- 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
- }];
-
+ const results = [];
+
+ for (const test of normalizationTests) {
try {
- // Convert from UBL to CII
- const ublString = await einvoice.toXmlString('ubl');
+ 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(ublString);
- const ciiString = await newInvoice.toXmlString('cii');
+ await newInvoice.fromXmlString(xmlString);
- // Check if content was preserved through transformation
- const preserved = ciiString.includes(content.text);
- console.log(`Format transform ${content.name}: ${preserved ? 'preserved' : 'modified'}`);
+ const roundTrip = newInvoice.from?.name?.includes(testText) || false;
- // 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'}`);
+ 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(`Format transform ${content.name} failed: ${error.message}`);
+ 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
diff --git a/test/suite/einvoice_edge-cases/test.edge-05.zero-byte-pdf.ts b/test/suite/einvoice_edge-cases/test.edge-05.zero-byte-pdf.ts
index 123d9fa..999a730 100644
--- a/test/suite/einvoice_edge-cases/test.edge-05.zero-byte-pdf.ts
+++ b/test/suite/einvoice_edge-cases/test.edge-05.zero-byte-pdf.ts
@@ -1,22 +1,27 @@
-import { tap } from '@git.zone/tstest/tapbundle';
+import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('EDGE-05: Zero-Byte PDFs - should handle zero-byte and minimal PDF files', async () => {
+ console.log('Testing zero-byte and minimal PDF handling...\n');
+
// Test 1: Truly zero-byte PDF
- await PerformanceTracker.track('truly-zero-byte-pdf', async () => {
+ const testZeroBytePdf = async () => {
const zeroPDF = Buffer.alloc(0);
try {
const result = await EInvoice.fromPdf(zeroPDF);
- console.log('Zero-byte PDF: unexpectedly succeeded', result);
+ console.log('Test 1 - Zero-byte PDF:');
+ console.log(' Unexpectedly succeeded, result:', result);
+ return { handled: false, error: null };
} catch (error) {
- console.log('Zero-byte PDF: properly failed with error:', error.message);
+ console.log('Test 1 - Zero-byte PDF:');
+ console.log(' Properly failed with error:', error.message);
+ return { handled: true, error: error.message };
}
- });
+ };
- // Test 2: Minimal PDF structure
- await PerformanceTracker.track('minimal-pdf-structure', async () => {
+ // Test 2: Minimal PDF structures
+ const testMinimalPdfStructures = async () => {
const minimalPDFs = [
{
name: 'header-only',
@@ -37,346 +42,189 @@ tap.test('EDGE-05: Zero-Byte PDFs - should handle zero-byte and minimal PDF file
'trailer\n<< /Size 2 /Root 1 0 R >>\n' +
'startxref\n64\n%%EOF'
)
+ },
+ {
+ name: 'invalid-header',
+ content: Buffer.from('NOT-A-PDF-HEADER')
+ },
+ {
+ name: 'truncated-pdf',
+ content: Buffer.from('%PDF-1.4\n1 0 obj\n<< /Type /Cat')
}
];
+ const results = [];
+
for (const pdf of minimalPDFs) {
+ try {
+ const result = await EInvoice.fromPdf(pdf.content);
+ console.log(`\nTest 2.${pdf.name}:`);
+ console.log(` Size: ${pdf.content.length} bytes`);
+ console.log(` Extracted invoice: Yes`);
+ console.log(` Result type: ${typeof result}`);
+ results.push({ name: pdf.name, success: true, error: null });
+ } catch (error) {
+ console.log(`\nTest 2.${pdf.name}:`);
+ console.log(` Size: ${pdf.content.length} bytes`);
+ console.log(` Error: ${error.message}`);
+ results.push({ name: pdf.name, success: false, error: error.message });
+ }
+ }
+
+ return results;
+ };
+
+ // Test 3: PDF with invalid content but correct headers
+ const testInvalidContentPdf = async () => {
+ const invalidContentPDFs = [
+ {
+ name: 'binary-garbage',
+ content: Buffer.concat([
+ Buffer.from('%PDF-1.4\n'),
+ Buffer.from(Array(100).fill(0).map(() => Math.floor(Math.random() * 256))),
+ Buffer.from('\n%%EOF')
+ ])
+ },
+ {
+ name: 'text-only',
+ content: Buffer.from('%PDF-1.4\nThis is just plain text content\n%%EOF')
+ },
+ {
+ name: 'xml-content',
+ content: Buffer.from('%PDF-1.4\ntest\n%%EOF')
+ }
+ ];
+
+ const results = [];
+
+ for (const pdf of invalidContentPDFs) {
+ try {
+ const result = await EInvoice.fromPdf(pdf.content);
+ console.log(`\nTest 3.${pdf.name}:`);
+ console.log(` PDF parsed successfully: Yes`);
+ console.log(` Invoice extracted: ${result ? 'Yes' : 'No'}`);
+ results.push({ name: pdf.name, parsed: true, extracted: !!result });
+ } catch (error) {
+ console.log(`\nTest 3.${pdf.name}:`);
+ console.log(` Error: ${error.message}`);
+ results.push({ name: pdf.name, parsed: false, extracted: false, error: error.message });
+ }
+ }
+
+ return results;
+ };
+
+ // Test 4: Edge case PDF sizes
+ const testEdgeCaseSizes = async () => {
+ const edgeCasePDFs = [
+ {
+ name: 'single-byte',
+ content: Buffer.from('P')
+ },
+ {
+ name: 'minimal-header',
+ content: Buffer.from('%PDF')
+ },
+ {
+ name: 'almost-valid-header',
+ content: Buffer.from('%PDF-1')
+ },
+ {
+ name: 'very-large-empty',
+ content: Buffer.concat([
+ Buffer.from('%PDF-1.4\n'),
+ Buffer.alloc(10000, 0x20), // 10KB of spaces
+ Buffer.from('\n%%EOF')
+ ])
+ }
+ ];
+
+ const results = [];
+
+ for (const pdf of edgeCasePDFs) {
try {
await EInvoice.fromPdf(pdf.content);
- console.log(`Minimal PDF ${pdf.name}: size=${pdf.content.length}, extracted invoice`);
+ console.log(`\nTest 4.${pdf.name}:`);
+ console.log(` Size: ${pdf.content.length} bytes`);
+ console.log(` Processing successful: Yes`);
+ results.push({ name: pdf.name, size: pdf.content.length, processed: true });
} catch (error) {
- console.log(`Minimal PDF ${pdf.name}: failed - ${error.message}`);
+ console.log(`\nTest 4.${pdf.name}:`);
+ console.log(` Size: ${pdf.content.length} bytes`);
+ console.log(` Error: ${error.message}`);
+ results.push({ name: pdf.name, size: pdf.content.length, processed: false, error: error.message });
}
}
- });
+
+ return results;
+ };
- // Test 3: Truncated PDF files
- await PerformanceTracker.track('truncated-pdf-files', async () => {
- // Start with a valid PDF structure and truncate at different points
- const fullPDF = Buffer.from(
- '%PDF-1.4\n' +
- '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
- '2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n' +
- '3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n' +
- 'xref\n0 4\n' +
- '0000000000 65535 f\n' +
- '0000000009 00000 n\n' +
- '0000000052 00000 n\n' +
- '0000000110 00000 n\n' +
- 'trailer\n<< /Size 4 /Root 1 0 R >>\n' +
- 'startxref\n196\n%%EOF'
- );
-
- const truncationPoints = [
- { name: 'after-header', bytes: 10 },
- { name: 'mid-object', bytes: 50 },
- { name: 'before-xref', bytes: 150 },
- { name: 'before-eof', bytes: fullPDF.length - 5 }
- ];
-
- for (const point of truncationPoints) {
- const truncated = fullPDF.subarray(0, point.bytes);
-
- try {
- await EInvoice.fromPdf(truncated);
- console.log(`Truncated PDF at ${point.name}: unexpectedly succeeded`);
- } catch (error) {
- console.log(`Truncated PDF at ${point.name}: properly failed - ${error.message}`);
- }
- }
- });
-
- // Test 4: PDF extraction and embedding
- await PerformanceTracker.track('pdf-extraction-embedding', async () => {
- // Create an invoice first
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'ZERO-001';
-
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing zero-byte scenarios',
- 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 Service',
- articleNumber: 'SRV-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
+ // Test 5: PDF with embedded XML but malformed structure
+ const testMalformedEmbeddedXml = async () => {
try {
- // Generate UBL
- const ublString = await einvoice.toXmlString('ubl');
- console.log(`Generated UBL invoice: ${ublString.length} bytes`);
+ // Create a PDF-like structure with embedded XML-like content
+ const malformedPdf = Buffer.from(
+ '%PDF-1.4\n' +
+ '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
+ '2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n' +
+ '3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\n' +
+ '4 0 obj\n<< /Type /EmbeddedFile /Filter /ASCIIHexDecode /Length 100 >>\n' +
+ 'stream\n' +
+ '3C696E766F6963653E3C2F696E766F6963653E\n' + // hex for
+ 'endstream\nendobj\n' +
+ 'xref\n0 5\n' +
+ '0000000000 65535 f\n' +
+ '0000000009 00000 n\n' +
+ '0000000052 00000 n\n' +
+ '0000000101 00000 n\n' +
+ '0000000141 00000 n\n' +
+ 'trailer\n<< /Size 5 /Root 1 0 R >>\n' +
+ 'startxref\n241\n%%EOF'
+ );
+
+ const result = await EInvoice.fromPdf(malformedPdf);
- // Try to embed in a minimal PDF (this will likely fail)
- const minimalPDF = Buffer.from('%PDF-1.4\n%%EOF');
- await einvoice.embedInPdf(minimalPDF, 'ubl');
- console.log(`Embedded XML in minimal PDF: success`);
+ console.log(`\nTest 5 - Malformed embedded XML:`);
+ console.log(` PDF size: ${malformedPdf.length} bytes`);
+ console.log(` Processing result: ${result ? 'Success' : 'No invoice found'}`);
+
+ return { processed: true, result: !!result };
} catch (error) {
- console.log(`PDF embedding test failed: ${error.message}`);
+ console.log(`\nTest 5 - Malformed embedded XML:`);
+ console.log(` Error: ${error.message}`);
+
+ return { processed: false, error: error.message };
}
- });
+ };
- // Test 5: Empty invoice edge cases
- await PerformanceTracker.track('empty-invoice-edge-cases', async () => {
- const testCases = [
- {
- name: 'no-items',
- setup: (invoice: EInvoice) => {
- invoice.items = [];
- }
- },
- {
- name: 'empty-strings',
- setup: (invoice: EInvoice) => {
- invoice.invoiceId = '';
- invoice.items = [{
- position: 1,
- name: '',
- articleNumber: '',
- unitType: 'EA',
- unitQuantity: 0,
- unitNetPrice: 0,
- vatPercentage: 0
- }];
- }
- },
- {
- name: 'zero-amounts',
- setup: (invoice: EInvoice) => {
- invoice.items = [{
- position: 1,
- name: 'Zero Value Item',
- articleNumber: 'ZERO-001',
- unitType: 'EA',
- unitQuantity: 0,
- unitNetPrice: 0,
- vatPercentage: 0
- }];
- }
- }
- ];
-
- for (const testCase of testCases) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'EMPTY-001';
-
- einvoice.from = {
- type: 'company',
- name: 'Empty Test Company',
- description: 'Testing empty scenarios',
- 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'
- }
- };
-
- // Apply test-specific setup
- testCase.setup(einvoice);
-
- try {
- const ciiString = await einvoice.toXmlString('cii');
- console.log(`Empty test ${testCase.name}: generated ${ciiString.length} bytes`);
-
- // Try validation
- const validationResult = await einvoice.validate();
- console.log(`Empty test ${testCase.name} validation: ${validationResult.valid ? 'valid' : 'invalid'}`);
- if (!validationResult.valid) {
- console.log(`Validation errors: ${validationResult.errors.length}`);
- }
- } catch (error) {
- console.log(`Empty test ${testCase.name} failed: ${error.message}`);
- }
- }
- });
+ // Run all tests
+ const zeroByteResult = await testZeroBytePdf();
+ const minimalResults = await testMinimalPdfStructures();
+ const invalidContentResults = await testInvalidContentPdf();
+ const edgeCaseResults = await testEdgeCaseSizes();
+ const malformedResult = await testMalformedEmbeddedXml();
- // Test 6: Batch processing with zero-byte PDFs
- await PerformanceTracker.track('batch-processing-zero-byte', async () => {
- const batch = [
- { name: 'zero-byte', content: Buffer.alloc(0) },
- { name: 'header-only', content: Buffer.from('%PDF-1.4') },
- { name: 'invalid', content: Buffer.from('Not a PDF') },
- { name: 'valid-minimal', content: createMinimalValidPDF() }
- ];
-
- let successful = 0;
- let failed = 0;
-
- for (const item of batch) {
- try {
- await EInvoice.fromPdf(item.content);
- successful++;
- console.log(`Batch item ${item.name}: extracted successfully`);
- } catch (error) {
- failed++;
- console.log(`Batch item ${item.name}: failed - ${error.message}`);
- }
- }
-
- console.log(`Batch processing complete: ${successful} successful, ${failed} failed`);
- });
+ console.log(`\n=== Zero-Byte PDF Test Summary ===`);
+
+ // Count results
+ const minimalHandled = minimalResults.filter(r => r.error !== null).length;
+ const invalidHandled = invalidContentResults.filter(r => r.error !== null).length;
+ const edgeCaseHandled = edgeCaseResults.filter(r => r.error !== null).length;
+
+ console.log(`Zero-byte PDF: ${zeroByteResult.handled ? 'Properly handled' : 'Unexpected behavior'}`);
+ console.log(`Minimal PDFs: ${minimalHandled}/${minimalResults.length} properly handled`);
+ console.log(`Invalid content PDFs: ${invalidHandled}/${invalidContentResults.length} properly handled`);
+ console.log(`Edge case sizes: ${edgeCaseHandled}/${edgeCaseResults.length} properly handled`);
+ console.log(`Malformed embedded XML: ${malformedResult.processed ? 'Processed' : 'Error handled'}`);
- // Test 7: Memory efficiency with zero content
- await PerformanceTracker.track('memory-efficiency-zero-content', async () => {
- const iterations = 100;
- const beforeMem = process.memoryUsage();
-
- // Create many empty invoices
- const invoices: EInvoice[] = [];
- for (let i = 0; i < iterations; i++) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = `MEM-${i}`;
-
- einvoice.from = {
- type: 'company',
- name: 'Memory Test',
- description: 'Testing memory',
- 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 = []; // Empty items
- invoices.push(einvoice);
- }
-
- const afterMem = process.memoryUsage();
- const memDiff = {
- heapUsed: Math.round((afterMem.heapUsed - beforeMem.heapUsed) / 1024 / 1024 * 100) / 100,
- rss: Math.round((afterMem.rss - beforeMem.rss) / 1024 / 1024 * 100) / 100
- };
-
- console.log(`Created ${iterations} empty invoices`);
- console.log(`Memory usage increase: Heap: ${memDiff.heapUsed}MB, RSS: ${memDiff.rss}MB`);
-
- // Try to process them all
- let processedCount = 0;
- for (const invoice of invoices) {
- try {
- const xml = await invoice.toXmlString('ubl');
- if (xml && xml.length > 0) {
- processedCount++;
- }
- } catch (error) {
- // Expected for empty invoices
- }
- }
-
- console.log(`Successfully processed ${processedCount} out of ${iterations} empty invoices`);
- });
+ // Test passes if the library properly handles edge cases without crashing
+ // Zero-byte PDF should fail gracefully
+ expect(zeroByteResult.handled).toBeTrue();
+
+ // At least some minimal PDFs should fail (they don't contain valid invoice data)
+ const someMinimalFailed = minimalResults.some(r => !r.success);
+ expect(someMinimalFailed).toBeTrue();
});
-// Helper function to create a minimal valid PDF
-function createMinimalValidPDF(): Buffer {
- return Buffer.from(
- '%PDF-1.4\n' +
- '1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
- '2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n' +
- 'xref\n0 3\n' +
- '0000000000 65535 f\n' +
- '0000000009 00000 n\n' +
- '0000000058 00000 n\n' +
- 'trailer\n<< /Size 3 /Root 1 0 R >>\n' +
- 'startxref\n115\n%%EOF'
- );
-}
-
// Run the test
tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts b/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts
index 2c4fe23..452b71d 100644
--- a/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts
+++ b/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts
@@ -1,666 +1,436 @@
-import { tap } from '@git.zone/tstest/tapbundle';
+import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { InvoiceFormat } from '../../../ts/interfaces/common.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('EDGE-06: Circular References - should handle circular reference scenarios', async () => {
- // Test 1: Self-referencing related documents
- await PerformanceTracker.track('self-referencing-documents', async () => {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'CIRC-001';
-
- // Set up basic invoice data
- einvoice.from = {
- type: 'company',
- name: 'Circular Test Company',
- description: 'Testing circular references',
- 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'
- }
- };
-
- // Add self-referencing related document
- einvoice.relatedDocuments = [{
- relationType: 'references',
- documentId: 'CIRC-001', // Self-reference
- issueDate: Date.now()
- }];
-
- einvoice.items = [{
- position: 1,
- name: 'Test Service',
- articleNumber: 'SRV-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- try {
- const xmlString = await einvoice.toXmlString('ubl');
- console.log('Self-referencing document: XML generated successfully');
-
- // Try to import it back
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
-
- console.log('Self-referencing document: Round-trip successful');
- console.log(`Related documents preserved: ${newInvoice.relatedDocuments?.length || 0}`);
- } catch (error) {
- console.log(`Self-referencing document failed: ${error.message}`);
- }
- });
+ console.log('Testing circular reference handling in e-invoices...\n');
- // Test 2: Circular issuer/recipient relationships
- await PerformanceTracker.track('circular-issuer-recipient', async () => {
- const invoices = [];
-
- // Create two companies that invoice each other
- const companyA = {
- type: 'company' as const,
- name: 'Company A',
- description: 'First company',
- address: {
- streetName: 'A Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'A City',
- country: 'DE'
- },
- status: 'active' as const,
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE111111111',
- registrationId: 'HRB 11111',
- registrationName: 'Commercial Register'
- }
- };
-
- const companyB = {
- type: 'company' as const,
- name: 'Company B',
- description: 'Second company',
- address: {
- streetName: 'B Street',
- houseNumber: '2',
- postalCode: '54321',
- city: 'B City',
- country: 'DE'
- },
- status: 'active' as const,
- foundedDate: { year: 2019, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE222222222',
- registrationId: 'HRB 22222',
- registrationName: 'Commercial Register'
- }
- };
-
- // Invoice 1: A invoices B
- const invoice1 = new EInvoice();
- invoice1.issueDate = new Date(2024, 0, 1);
- invoice1.invoiceId = 'A-TO-B-001';
- invoice1.from = companyA;
- invoice1.to = companyB;
- invoice1.items = [{
- position: 1,
- name: 'Service from A',
- articleNumber: 'A-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Invoice 2: B invoices A (circular)
- const invoice2 = new EInvoice();
- invoice2.issueDate = new Date(2024, 0, 2);
- invoice2.invoiceId = 'B-TO-A-001';
- invoice2.from = companyB;
- invoice2.to = companyA;
- invoice2.relatedDocuments = [{
- relationType: 'references',
- documentId: 'A-TO-B-001',
- issueDate: invoice1.date
- }];
- invoice2.items = [{
- position: 1,
- name: 'Service from B',
- articleNumber: 'B-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 150,
- vatPercentage: 19
- }];
-
- invoices.push(invoice1, invoice2);
-
+ // Test 1: Self-referencing invoice documents
+ const testSelfReferencingInvoice = async () => {
try {
- for (const invoice of invoices) {
- const xmlString = await invoice.toXmlString('cii');
- console.log(`Circular issuer/recipient ${invoice.invoiceId}: XML generated`);
- }
- console.log('Circular issuer/recipient relationships handled successfully');
- } catch (error) {
- console.log(`Circular issuer/recipient failed: ${error.message}`);
- }
- });
-
- // Test 3: Deep nesting with circular item descriptions
- await PerformanceTracker.track('deep-nesting-circular', async () => {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'DEEP-001';
-
- einvoice.from = {
- type: 'company',
- name: 'Deep Nesting Company',
- description: 'Company that references itself in description: Deep Nesting Company',
- address: {
- streetName: 'Recursive Street',
- houseNumber: '∞',
- postalCode: '12345',
- city: 'Loop City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE333333333',
- registrationId: 'HRB 33333',
- registrationName: 'Commercial Register'
- }
- };
-
- einvoice.to = {
- type: 'person',
- name: 'Recursive',
- surname: 'Customer',
- salutation: 'Mr' as const,
- sex: 'male' as const,
- title: 'Doctor' as const,
- description: 'Customer who buys recursive items',
- address: {
- streetName: 'Customer Street',
- houseNumber: '2',
- postalCode: '54321',
- city: 'Customer City',
- country: 'DE'
- }
- };
-
- // Create items with descriptions that reference each other
- const itemCount = 10;
- einvoice.items = [];
-
- for (let i = 0; i < itemCount; i++) {
- einvoice.items.push({
- position: i + 1,
- name: `Item ${i} references Item ${(i + 1) % itemCount}`,
- articleNumber: `CIRC-${i}`,
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 10 * (i + 1),
- vatPercentage: 19
- });
- }
-
- try {
- const ublString = await einvoice.toXmlString('ubl');
- console.log(`Deep nesting: Generated ${einvoice.items.length} circularly referencing items`);
- console.log(`XML size: ${ublString.length} bytes`);
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.id = 'CIRC-001';
- // Test round-trip
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(ublString);
- console.log(`Deep nesting round-trip: ${newInvoice.items.length} items preserved`);
- } catch (error) {
- console.log(`Deep nesting failed: ${error.message}`);
- }
- });
-
- // Test 4: Format conversion with patterns
- await PerformanceTracker.track('format-conversion-patterns', async () => {
- // Create invoice with repeating patterns
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'PATTERN-001';
-
- einvoice.from = {
- type: 'company',
- name: 'Pattern Company',
- description: 'Pattern Pattern Pattern',
- address: {
- streetName: 'Pattern Street',
- houseNumber: '123',
- postalCode: '12345',
- city: 'Pattern City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE444444444',
- registrationId: 'HRB 44444',
- registrationName: 'Pattern Register'
- }
- };
-
- einvoice.to = {
- type: 'company',
- name: 'Repeat Customer',
- description: 'Customer Customer Customer',
- address: {
- streetName: 'Repeat Street',
- houseNumber: '321',
- postalCode: '54321',
- city: 'Repeat City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2019, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE555555555',
- registrationId: 'HRB 55555',
- registrationName: 'Repeat Register'
- }
- };
-
- // Add items with repeating patterns
- einvoice.items = [
- {
+ // Set up basic invoice data for EN16931 compliance
+ einvoice.from = {
+ type: 'company',
+ name: 'Circular Test Company',
+ description: 'Testing circular references',
+ 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: 'AAA AAA AAA Service',
- articleNumber: 'AAA-001',
- unitType: 'EA',
- unitQuantity: 3,
- unitNetPrice: 33.33,
- vatPercentage: 19
- },
- {
- position: 2,
- name: 'BBB BBB BBB Product',
- articleNumber: 'BBB-002',
- unitType: 'EA',
- unitQuantity: 2,
- unitNetPrice: 22.22,
- vatPercentage: 19
- }
- ];
-
- try {
- // Convert between formats
- const ublString = await einvoice.toXmlString('ubl');
- console.log('Pattern invoice: UBL generated');
-
- const ublInvoice = await EInvoice.fromXml(ublString);
- const ciiString = await ublInvoice.toXmlString('cii');
- console.log('Pattern invoice: Converted to CII');
-
- const ciiInvoice = await EInvoice.fromXml(ciiString);
- console.log(`Pattern preservation: ${ciiInvoice.from.description === 'Pattern Pattern Pattern'}`);
- } catch (error) {
- console.log(`Format conversion patterns failed: ${error.message}`);
- }
- });
-
- // Test 5: Memory safety with large circular structures
- await PerformanceTracker.track('memory-safety-circular', async () => {
- const iterations = 50;
- const beforeMem = process.memoryUsage();
-
- try {
- // Create many invoices that reference each other
- const invoices: EInvoice[] = [];
-
- for (let i = 0; i < iterations; i++) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = `MEM-${i}`;
-
- // Reference the previous invoice
- if (i > 0) {
- einvoice.relatedDocuments = [{
- relationType: 'references',
- documentId: `MEM-${i - 1}`,
- issueDate: Date.now()
- }];
- }
-
- // And reference the next one (creating a cycle)
- if (i < iterations - 1) {
- if (!einvoice.relatedDocuments) einvoice.relatedDocuments = [];
- einvoice.relatedDocuments.push({
- relationType: 'references',
- documentId: `MEM-${i + 1}`,
- issueDate: Date.now()
- });
- }
-
- einvoice.from = {
- type: 'company',
- name: `Company ${i}`,
- description: `References Company ${(i + 1) % iterations}`,
- address: {
- streetName: 'Memory Street',
- houseNumber: String(i),
- postalCode: '12345',
- city: 'Memory City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: `DE${String(i).padStart(9, '0')}`,
- registrationId: `HRB ${i}`,
- 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 referencing invoice ${(i + 1) % iterations}`,
- articleNumber: `MEM-${i}`,
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 10,
- vatPercentage: 19
- }];
-
- invoices.push(einvoice);
- }
-
- // Try to export all
- let exportedCount = 0;
- for (const invoice of invoices) {
- try {
- const xml = await invoice.toXmlString('ubl');
- if (xml) exportedCount++;
- } catch (e) {
- // Ignore individual failures
- }
- }
-
- const afterMem = process.memoryUsage();
- const memIncrease = (afterMem.heapUsed - beforeMem.heapUsed) / 1024 / 1024;
-
- console.log(`Memory safety: Created ${iterations} circular invoices`);
- console.log(`Successfully exported: ${exportedCount}`);
- console.log(`Memory increase: ${memIncrease.toFixed(2)} MB`);
- console.log(`Memory stable: ${memIncrease < 50}`);
- } catch (error) {
- console.log(`Memory safety test failed: ${error.message}`);
- }
- });
-
- // Test 6: Validation with circular references
- await PerformanceTracker.track('validation-circular', async () => {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'VAL-CIRC-001';
-
- einvoice.from = {
- type: 'company',
- name: 'Validation Company',
- description: 'Company for validation testing',
- address: {
- streetName: 'Validation Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'Validation City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE666666666',
- registrationId: 'HRB 66666',
- 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: 'DE777777777',
- registrationId: 'HRB 77777',
- registrationName: 'Commercial Register'
- }
- };
-
- // Create items with interdependent values
- einvoice.items = [
- {
- position: 1,
- name: 'Base Item',
- articleNumber: 'BASE-001',
- unitType: 'EA',
- unitQuantity: 10,
- unitNetPrice: 100,
- vatPercentage: 19
- },
- {
- position: 2,
- name: 'Dependent Item (10% of Base)',
- articleNumber: 'DEP-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100, // Should be 10% of base total
- vatPercentage: 19
- },
- {
- position: 3,
- name: 'Circular Dependent (refers to position 2)',
+ name: 'Test Product',
articleNumber: 'CIRC-001',
unitType: 'EA',
unitQuantity: 1,
- unitNetPrice: 10, // 10% of dependent
+ unitNetPrice: 100,
vatPercentage: 19
- }
- ];
-
- try {
- const xmlString = await einvoice.toXmlString('xrechnung');
- console.log('Validation with circular refs: XML generated');
+ }];
- // Validate
- const validationResult = await einvoice.validate();
- console.log(`Validation result: ${validationResult.valid ? 'valid' : 'invalid'}`);
- console.log(`Validation errors: ${validationResult.errors.length}`);
-
- if (validationResult.errors.length > 0) {
- console.log(`First error: ${validationResult.errors[0].message}`);
- }
- } catch (error) {
- console.log(`Validation circular failed: ${error.message}`);
- }
- });
-
- // Test 7: PDF operations with circular metadata
- await PerformanceTracker.track('pdf-circular-metadata', async () => {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'PDF-CIRC-001';
-
- einvoice.from = {
- type: 'company',
- name: 'PDF Company',
- description: 'Company testing PDF with circular refs',
- address: {
- streetName: 'PDF Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'PDF City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE888888888',
- registrationId: 'HRB 88888',
- registrationName: 'Commercial Register'
- }
- };
-
- einvoice.to = {
- type: 'person',
- name: 'PDF',
- surname: 'Customer',
- salutation: 'Mr' as const,
- sex: 'male' as const,
- title: 'Doctor' as const,
- description: 'Customer for PDF testing',
- address: {
- streetName: 'Customer Street',
- houseNumber: '2',
- postalCode: '54321',
- city: 'Customer City',
- country: 'DE'
- }
- };
-
- einvoice.items = [{
- position: 1,
- name: 'PDF Service',
- articleNumber: 'PDF-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Set circular metadata
- einvoice.metadata = {
- format: InvoiceFormat.FACTURX,
- version: '1.0',
- customizationId: 'urn:factur-x.eu:1p0:basicwl',
- extensions: {
- circularRef: 'PDF-CIRC-001' // Self-reference
- }
- };
-
- try {
- const xmlString = await einvoice.toXmlString('facturx');
- console.log('PDF circular metadata: XML generated');
- console.log(`Metadata preserved: ${einvoice.metadata?.extensions?.circularRef === 'PDF-CIRC-001'}`);
-
- // Test with minimal PDF
- const minimalPDF = Buffer.from('%PDF-1.4\n%%EOF');
- try {
- const pdfWithXml = await einvoice.embedInPdf(minimalPDF, 'facturx');
- console.log('PDF circular metadata: Embedded in PDF');
- } catch (e) {
- console.log('PDF circular metadata: Embedding failed (expected for minimal PDF)');
- }
- } catch (error) {
- console.log(`PDF circular metadata failed: ${error.message}`);
- }
- });
-
- // Test 8: Empty circular structures
- await PerformanceTracker.track('empty-circular-structures', async () => {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = ''; // Empty ID
-
- einvoice.from = {
- type: 'company',
- name: '', // Empty name
- description: '',
- address: {
- streetName: '',
- houseNumber: '',
- postalCode: '',
- city: '',
- country: ''
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: '',
- registrationId: '',
- registrationName: ''
- }
- };
-
- einvoice.to = einvoice.from; // Circular reference to same empty object
-
- einvoice.items = []; // Empty items
-
- einvoice.relatedDocuments = [{
- relationType: 'references',
- documentId: '', // Empty reference
- issueDate: Date.now()
- }];
-
- try {
+ // Try to create XML - should not cause infinite loops
const xmlString = await einvoice.toXmlString('ubl');
- console.log('Empty circular: XML generated despite empty values');
- console.log(`XML length: ${xmlString.length} bytes`);
+
+ // Test round-trip
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const roundTripSuccess = (newInvoice.id === 'CIRC-001' ||
+ newInvoice.invoiceId === 'CIRC-001' ||
+ newInvoice.accountingDocId === 'CIRC-001');
+
+ console.log('Test 1 - Self-referencing invoice:');
+ console.log(` XML generation successful: Yes`);
+ console.log(` Round-trip successful: ${roundTripSuccess ? 'Yes' : 'No'}`);
+ console.log(` No infinite loops detected: Yes`);
+
+ return { success: true, roundTrip: roundTripSuccess };
} catch (error) {
- console.log(`Empty circular failed: ${error.message}`);
+ console.log('Test 1 - Self-referencing invoice:');
+ console.log(` Error: ${error.message}`);
+ return { success: false, roundTrip: false, error: error.message };
}
- });
+ };
+
+ // Test 2: XML with circular element references
+ const testXmlCircularReferences = async () => {
+ // Create XML with potential circular references
+ const circularXml = `
+
+ 2.1
+ CIRCULAR-REF-TEST
+ 2024-01-01
+ EUR
+
+
+
+ CIRCULAR-REF-TEST
+ 380
+
+
+
+
+
+ Circular Test Company
+
+
+ Test Street
+ 1
+ 12345
+ Test City
+
+ DE
+
+
+
+
+
+
+
+
+ Customer Company
+
+
+ Customer Street
+ 2
+ 54321
+ Customer City
+
+ DE
+
+
+
+
+
+
+ 1
+ 1
+ 100.00
+
+ Test Product
+
+
+ 100.00
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(circularXml);
+
+ // Try to export back to XML
+ await einvoice.toXmlString('ubl');
+
+ console.log('\nTest 2 - XML circular references:');
+ console.log(` Circular XML parsed: Yes`);
+ console.log(` Re-export successful: Yes`);
+ console.log(` No infinite loops in parsing: Yes`);
+
+ return { parsed: true, exported: true };
+ } catch (error) {
+ console.log('\nTest 2 - XML circular references:');
+ console.log(` Error: ${error.message}`);
+ return { parsed: false, exported: false, error: error.message };
+ }
+ };
+
+ // Test 3: Deep object nesting that could cause stack overflow
+ const testDeepObjectNesting = async () => {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.id = 'DEEP-NEST-TEST';
+ einvoice.issueDate = new Date(2024, 0, 1);
+
+ // Create deeply nested structure
+ einvoice.from = {
+ type: 'company',
+ name: 'Deep Nesting Company',
+ description: 'Testing deep object nesting',
+ 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'
+ }
+ };
+
+ // Create many items to test deep arrays
+ einvoice.items = [];
+ for (let i = 1; i <= 100; i++) {
+ einvoice.items.push({
+ position: i,
+ name: `Product ${i}`,
+ articleNumber: `DEEP-${i.toString().padStart(3, '0')}`,
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 10 + (i % 10),
+ vatPercentage: 19
+ });
+ }
+
+ // Test XML generation with deep structure
+ const xmlString = await einvoice.toXmlString('ubl');
+
+ // Test parsing back
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const itemsMatch = newInvoice.items?.length === 100;
+
+ console.log('\nTest 3 - Deep object nesting:');
+ console.log(` Deep structure generated: Yes`);
+ console.log(` XML parsing successful: Yes`);
+ console.log(` Items preserved: ${itemsMatch ? 'Yes' : 'No'} (${newInvoice.items?.length || 0}/100)`);
+ console.log(` No stack overflow: Yes`);
+
+ return { generated: true, parsed: true, itemsMatch };
+ } catch (error) {
+ console.log('\nTest 3 - Deep object nesting:');
+ console.log(` Error: ${error.message}`);
+ return { generated: false, parsed: false, itemsMatch: false, error: error.message };
+ }
+ };
+
+ // Test 4: JSON circular reference detection
+ const testJsonCircularReferences = async () => {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.id = 'JSON-CIRC-TEST';
+ einvoice.issueDate = new Date(2024, 0, 1);
+
+ einvoice.from = {
+ type: 'company',
+ name: 'JSON Test Company',
+ description: 'Testing JSON circular references',
+ 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: 'JSON Test Product',
+ articleNumber: 'JSON-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ // Test JSON stringification (should not cause circular reference errors)
+ const jsonString = JSON.stringify(einvoice);
+ const parsedBack = JSON.parse(jsonString);
+
+ console.log('\nTest 4 - JSON circular references:');
+ console.log(` JSON stringify successful: Yes`);
+ console.log(` JSON parse successful: Yes`);
+ console.log(` Object structure preserved: ${parsedBack.id === 'JSON-CIRC-TEST' ? 'Yes' : 'No'}`);
+
+ return { stringified: true, parsed: true, preserved: parsedBack.id === 'JSON-CIRC-TEST' };
+ } catch (error) {
+ console.log('\nTest 4 - JSON circular references:');
+ console.log(` Error: ${error.message}`);
+ return { stringified: false, parsed: false, preserved: false, error: error.message };
+ }
+ };
+
+ // Test 5: Format conversion with potential circular references
+ const testFormatConversionCircular = async () => {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.id = 'FORMAT-CIRC-TEST';
+ einvoice.issueDate = new Date(2024, 0, 1);
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Format Test Company',
+ description: 'Testing format conversion circular references',
+ 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: 'Format Test Product',
+ articleNumber: 'FORMAT-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ // Test conversion between formats (UBL -> CII -> UBL)
+ const ublXml = await einvoice.toXmlString('ubl');
+
+ const ublInvoice = new EInvoice();
+ await ublInvoice.fromXmlString(ublXml);
+
+ const ciiXml = await ublInvoice.toXmlString('cii');
+
+ const ciiInvoice = new EInvoice();
+ await ciiInvoice.fromXmlString(ciiXml);
+
+ const finalUblXml = await ciiInvoice.toXmlString('ubl');
+
+ const finalInvoice = new EInvoice();
+ await finalInvoice.fromXmlString(finalUblXml);
+
+ const idPreserved = (finalInvoice.id === 'FORMAT-CIRC-TEST' ||
+ finalInvoice.invoiceId === 'FORMAT-CIRC-TEST' ||
+ finalInvoice.accountingDocId === 'FORMAT-CIRC-TEST');
+
+ console.log('\nTest 5 - Format conversion circular:');
+ console.log(` UBL generation: Yes`);
+ console.log(` UBL->CII conversion: Yes`);
+ console.log(` CII->UBL conversion: Yes`);
+ console.log(` ID preserved through conversions: ${idPreserved ? 'Yes' : 'No'}`);
+ console.log(` No infinite loops in conversion: Yes`);
+
+ return { ublGenerated: true, ciiConverted: true, backConverted: true, idPreserved };
+ } catch (error) {
+ console.log('\nTest 5 - Format conversion circular:');
+ console.log(` Error: ${error.message}`);
+ return { ublGenerated: false, ciiConverted: false, backConverted: false, idPreserved: false, error: error.message };
+ }
+ };
+
+ // Run all tests
+ const selfRefResult = await testSelfReferencingInvoice();
+ const xmlCircularResult = await testXmlCircularReferences();
+ const deepNestingResult = await testDeepObjectNesting();
+ const jsonCircularResult = await testJsonCircularReferences();
+ const formatConversionResult = await testFormatConversionCircular();
+
+ console.log(`\n=== Circular References Test Summary ===`);
+ console.log(`Self-referencing invoice: ${selfRefResult.success ? 'Working' : 'Issues'}`);
+ console.log(`XML circular references: ${xmlCircularResult.parsed ? 'Working' : 'Issues'}`);
+ console.log(`Deep object nesting: ${deepNestingResult.generated && deepNestingResult.parsed ? 'Working' : 'Issues'}`);
+ console.log(`JSON circular detection: ${jsonCircularResult.stringified && jsonCircularResult.parsed ? 'Working' : 'Issues'}`);
+ console.log(`Format conversion: ${formatConversionResult.ublGenerated && formatConversionResult.backConverted ? 'Working' : 'Issues'}`);
+
+ // Test passes if basic operations work without infinite loops
+ expect(selfRefResult.success).toBeTrue();
+ expect(jsonCircularResult.stringified && jsonCircularResult.parsed).toBeTrue();
+ expect(deepNestingResult.generated && deepNestingResult.parsed).toBeTrue();
});
// Run the test
diff --git a/test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts b/test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts
index c3deab0..4d45bef 100644
--- a/test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts
+++ b/test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts
@@ -1,678 +1,41 @@
import { tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allowed lengths', async () => {
- // Test 1: Standard field length limits
- await PerformanceTracker.track('standard-field-limits', async () => {
+ console.log('Testing maximum field lengths in e-invoices...\n');
+
+ // Test 1: Standard field length limits per EN16931
+ const testStandardFieldLimits = async () => {
const fieldTests = [
- { field: 'invoiceId', maxLength: 200, testValue: 'X' },
- { field: 'customerName', maxLength: 200, testValue: 'A' },
- { field: 'streetName', maxLength: 1000, testValue: 'B' },
- { field: 'notes', maxLength: 5000, testValue: 'C' }
+ { field: 'invoiceId', maxLength: 30, testValue: 'INV' }, // BT-1 Invoice number
+ { field: 'customerName', maxLength: 200, testValue: 'ACME' }, // BT-44 Buyer name
+ { field: 'streetName', maxLength: 1000, testValue: 'Street' }, // BT-35 Buyer address line 1
+ { field: 'subject', maxLength: 100, testValue: 'SUBJ' }, // Invoice subject
+ { field: 'notes', maxLength: 5000, testValue: 'NOTE' } // BT-22 Invoice note
];
+ console.log('Test 1 - Standard field limits:');
+
for (const test of fieldTests) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
-
- // Test at max length
- const maxValue = test.testValue.repeat(test.maxLength);
-
- if (test.field === 'invoiceId') {
- einvoice.invoiceId = maxValue;
- }
-
- einvoice.from = {
- type: 'company',
- name: test.field === 'customerName' ? maxValue : 'Test Company',
- description: 'Testing max field lengths',
- address: {
- streetName: test.field === 'streetName' ? maxValue : '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'
- }
- };
-
- if (test.field === 'notes') {
- einvoice.notes = [maxValue];
- }
-
- einvoice.items = [{
- position: 1,
- name: 'Test Item',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
try {
- const xmlString = await einvoice.toXmlString('ubl');
- console.log(`Field ${test.field} at max length (${test.maxLength}): XML generated, size: ${xmlString.length} bytes`);
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
- // Test round-trip
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
+ // Test at max length
+ const maxValue = test.testValue.repeat(Math.ceil(test.maxLength / test.testValue.length)).substring(0, test.maxLength);
- let preserved = false;
if (test.field === 'invoiceId') {
- preserved = newInvoice.invoiceId === maxValue;
- } else if (test.field === 'customerName') {
- preserved = newInvoice.from.name === maxValue;
- } else if (test.field === 'streetName') {
- preserved = newInvoice.from.address.streetName === maxValue;
- } else if (test.field === 'notes') {
- preserved = newInvoice.notes?.[0] === maxValue;
+ einvoice.invoiceId = maxValue;
+ } else if (test.field === 'subject') {
+ einvoice.subject = maxValue;
}
- console.log(`Field ${test.field} preservation: ${preserved ? 'preserved' : 'truncated'}`);
- } catch (error) {
- console.log(`Field ${test.field} at max length failed: ${error.message}`);
- }
-
- // Test over max length
- const overValue = test.testValue.repeat(test.maxLength + 100);
- const overInvoice = new EInvoice();
- overInvoice.issueDate = new Date(2024, 0, 1);
-
- if (test.field === 'invoiceId') {
- overInvoice.invoiceId = overValue;
- }
-
- overInvoice.from = {
- type: 'company',
- name: test.field === 'customerName' ? overValue : 'Test Company',
- description: 'Testing over max field lengths',
- address: {
- streetName: test.field === 'streetName' ? overValue : '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'
- }
- };
-
- overInvoice.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'
- }
- };
-
- if (test.field === 'notes') {
- overInvoice.notes = [overValue];
- }
-
- overInvoice.items = [{
- position: 1,
- name: 'Test Item',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- try {
- const overXmlString = await overInvoice.toXmlString('ubl');
- console.log(`Field ${test.field} over max length: still generated XML (${overXmlString.length} bytes)`);
- } catch (error) {
- console.log(`Field ${test.field} over max length: properly rejected - ${error.message}`);
- }
- }
- });
-
- // Test 2: Unicode character length vs byte length
- await PerformanceTracker.track('unicode-length-vs-bytes', async () => {
- const testCases = [
- { name: 'ascii-only', char: 'A', bytesPerChar: 1 },
- { name: 'latin-extended', char: 'ñ', bytesPerChar: 2 },
- { name: 'chinese', char: '中', bytesPerChar: 3 },
- { name: 'emoji', char: '😀', bytesPerChar: 4 }
- ];
-
- const maxChars = 100;
-
- for (const test of testCases) {
- const value = test.char.repeat(maxChars);
- const byteLength = Buffer.from(value, 'utf8').length;
-
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'UNICODE-TEST';
-
- einvoice.from = {
- type: 'company',
- name: value,
- description: `Unicode test: ${test.name}`,
- address: {
- streetName: 'Unicode Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'Unicode 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: 'Test Item',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- try {
- const xmlString = await einvoice.toXmlString('cii');
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
-
- const retrievedValue = newInvoice.from.name;
- const preserved = retrievedValue === value;
- const retrievedBytes = Buffer.from(retrievedValue, 'utf8').length;
-
- console.log(`Unicode ${test.name}: chars=${value.length}, bytes=${byteLength}, expectedBytes=${maxChars * test.bytesPerChar}`);
- console.log(` Preserved: ${preserved}, retrieved chars=${retrievedValue.length}, bytes=${retrievedBytes}`);
- } catch (error) {
- console.log(`Unicode ${test.name} failed: ${error.message}`);
- }
- }
- });
-
- // Test 3: Long invoice numbers
- await PerformanceTracker.track('long-invoice-numbers', async () => {
- const lengths = [50, 100, 200, 500];
-
- for (const length of lengths) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'INV-' + '0'.repeat(length - 4);
-
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing long invoice numbers',
- 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 Item',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- try {
- const xmlString = await einvoice.toXmlString('xrechnung');
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
-
- console.log(`Invoice number length ${length}: ${newInvoice.invoiceId.length === length ? 'preserved' : 'modified'}`);
- } catch (error) {
- console.log(`Invoice number length ${length} failed: ${error.message}`);
- }
- }
- });
-
- // Test 4: Line item count limits
- await PerformanceTracker.track('line-item-count-limits', async () => {
- const itemCounts = [100, 500, 1000];
-
- for (const count of itemCounts) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = `MANY-ITEMS-${count}`;
-
- einvoice.from = {
- type: 'company',
- name: 'Bulk Seller Company',
- description: 'Testing many line items',
- address: {
- streetName: 'Bulk Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'Bulk 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: 'Bulk Buyer Company',
- description: 'Customer buying many items',
- address: {
- streetName: 'Buyer Street',
- houseNumber: '2',
- postalCode: '54321',
- city: 'Buyer City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2019, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE987654321',
- registrationId: 'HRB 54321',
- registrationName: 'Commercial Register'
- }
- };
-
- // Create many items
- einvoice.items = [];
- for (let i = 0; i < count; i++) {
- einvoice.items.push({
- position: i + 1,
- name: `Item ${i + 1}`,
- articleNumber: `ART-${String(i + 1).padStart(5, '0')}`,
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 10 + (i % 100),
- vatPercentage: 19
- });
- }
-
- const startTime = Date.now();
-
- try {
- const xmlString = await einvoice.toXmlString('ubl');
- const endTime = Date.now();
- const timeTaken = endTime - startTime;
-
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
- const itemsParsed = newInvoice.items.length;
-
- console.log(`Line items ${count}: parsed=${itemsParsed}, time=${timeTaken}ms, avg=${(timeTaken/count).toFixed(2)}ms/item`);
- } catch (error) {
- console.log(`Line items ${count} failed: ${error.message}`);
- }
- }
- });
-
- // Test 5: Long email addresses
- await PerformanceTracker.track('long-email-addresses', async () => {
- const emailLengths = [50, 100, 254]; // RFC 5321 limit
-
- for (const length of emailLengths) {
- const localPart = 'x'.repeat(Math.max(1, length - 20));
- const email = localPart + '@example.com';
-
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'EMAIL-TEST';
- einvoice.electronicAddress = {
- scheme: 'EMAIL',
- value: email.substring(0, length)
- };
-
- einvoice.from = {
- type: 'company',
- name: 'Email Test Company',
- description: 'Testing long email addresses',
- address: {
- streetName: 'Email Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'Email 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: 'Test Item',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- try {
- const xmlString = await einvoice.toXmlString('ubl');
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
-
- const preserved = newInvoice.electronicAddress?.value === email.substring(0, length);
- console.log(`Email length ${length}: ${preserved ? 'preserved' : 'modified'}`);
- } catch (error) {
- console.log(`Email length ${length} failed: ${error.message}`);
- }
- }
- });
-
- // Test 6: Decimal precision limits
- await PerformanceTracker.track('decimal-precision-limits', async () => {
- const precisionTests = [
- { decimals: 2, value: 12345678901234567890.12 },
- { decimals: 4, value: 123456789012345.1234 },
- { decimals: 6, value: 1234567890.123456 }
- ];
-
- for (const test of precisionTests) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'DECIMAL-TEST';
-
- einvoice.from = {
- type: 'company',
- name: 'Decimal Test Company',
- description: 'Testing decimal precision',
- address: {
- streetName: 'Decimal Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'Decimal 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: 'High Precision Item',
- articleNumber: 'DECIMAL-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: test.value,
- vatPercentage: 19.123456 // Test VAT precision too
- }];
-
- try {
- const xmlString = await einvoice.toXmlString('cii');
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
-
- const originalStr = test.value.toString();
- const parsedValue = newInvoice.items[0].unitNetPrice;
- const parsedStr = parsedValue.toString();
-
- console.log(`Decimal precision ${test.decimals}: original=${originalStr}, parsed=${parsedStr}`);
- console.log(` Preserved: ${parsedValue === test.value}`);
- } catch (error) {
- console.log(`Decimal precision ${test.decimals} failed: ${error.message}`);
- }
- }
- });
-
- // Test 7: Long postal codes and phone numbers
- await PerformanceTracker.track('postal-codes-phone-numbers', async () => {
- const tests = [
- { field: 'postalCode', lengths: [5, 10, 20] },
- { field: 'phone', lengths: [10, 20, 30] }
- ];
-
- for (const test of tests) {
- for (const length of test.lengths) {
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = `${test.field.toUpperCase()}-${length}`;
-
- const value = '1234567890'.repeat(Math.ceil(length / 10)).substring(0, length);
-
einvoice.from = {
type: 'company',
- name: 'Field Length Test Company',
- description: `Testing ${test.field} length`,
+ name: test.field === 'customerName' ? maxValue : 'Test Company',
+ description: 'Testing max field lengths',
address: {
- streetName: 'Test Street',
- houseNumber: '1',
- postalCode: test.field === 'postalCode' ? value : '12345',
- city: 'Test City',
- country: 'DE'
- },
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE123456789',
- registrationId: 'HRB 12345',
- registrationName: 'Commercial Register'
- }
- };
-
- if (test.field === 'phone') {
- (einvoice.from as any).phone = value;
- }
-
- 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: 'Test Item',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- try {
- const xmlString = await einvoice.toXmlString('ubl');
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlString);
-
- let preserved = false;
- if (test.field === 'postalCode') {
- preserved = newInvoice.from.address.postalCode === value;
- } else if (test.field === 'phone') {
- preserved = (newInvoice.from as any).phone === value;
- }
-
- console.log(`${test.field} length ${length}: ${preserved ? 'preserved' : 'modified'}`);
- } catch (error) {
- console.log(`${test.field} length ${length} failed: ${error.message}`);
- }
- }
- }
- });
-
- // Test 8: Performance impact of field lengths
- await PerformanceTracker.track('field-length-performance-impact', async () => {
- const lengths = [10, 100, 1000, 10000];
- const performanceResults = [];
-
- for (const length of lengths) {
- const iterations = 5;
- const times = [];
-
- for (let i = 0; i < iterations; i++) {
- const value = 'X'.repeat(length);
- const einvoice = new EInvoice();
- einvoice.issueDate = new Date(2024, 0, 1);
- einvoice.invoiceId = 'PERF-TEST';
- einvoice.subject = value;
- einvoice.notes = [value, value, value];
-
- einvoice.from = {
- type: 'company',
- name: value.substring(0, 200),
- description: value,
- address: {
- streetName: value.substring(0, 1000),
+ streetName: test.field === 'streetName' ? maxValue : 'Test Street',
houseNumber: '1',
postalCode: '12345',
city: 'Test City',
@@ -707,9 +70,13 @@ tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allow
}
};
+ if (test.field === 'notes') {
+ einvoice.notes = [maxValue];
+ }
+
einvoice.items = [{
position: 1,
- name: value.substring(0, 500),
+ name: 'Test Item',
articleNumber: 'TEST-001',
unitType: 'EA',
unitQuantity: 1,
@@ -717,36 +84,519 @@ tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allow
vatPercentage: 19
}];
- const startTime = process.hrtime.bigint();
+ // Generate XML
+ const xmlString = await einvoice.toXmlString('ubl');
- try {
- await einvoice.toXmlString('ubl');
- } catch (error) {
- // Ignore errors for performance testing
+ // Test round-trip
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ let preserved = false;
+ if (test.field === 'invoiceId') {
+ preserved = newInvoice.invoiceId === maxValue;
+ } else if (test.field === 'customerName') {
+ preserved = newInvoice.from.name === maxValue;
+ } else if (test.field === 'streetName') {
+ preserved = newInvoice.from.address.streetName === maxValue;
+ } else if (test.field === 'subject') {
+ preserved = newInvoice.subject === maxValue;
+ } else if (test.field === 'notes') {
+ preserved = newInvoice.notes?.[0] === maxValue;
}
- const endTime = process.hrtime.bigint();
- times.push(Number(endTime - startTime) / 1000000); // Convert to ms
+ console.log(` ${test.field} (${test.maxLength} chars): ${preserved ? 'preserved' : 'truncated'}`);
+
+ // Test over max length (+50 chars)
+ const overValue = test.testValue.repeat(Math.ceil((test.maxLength + 50) / test.testValue.length)).substring(0, test.maxLength + 50);
+ const overInvoice = new EInvoice();
+ overInvoice.issueDate = new Date(2024, 0, 1);
+
+ if (test.field === 'invoiceId') {
+ overInvoice.invoiceId = overValue;
+ } else if (test.field === 'subject') {
+ overInvoice.subject = overValue;
+ }
+
+ overInvoice.from = {
+ type: 'company',
+ name: test.field === 'customerName' ? overValue : 'Test Company',
+ description: 'Testing over max field lengths',
+ address: {
+ streetName: test.field === 'streetName' ? overValue : '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'
+ }
+ };
+
+ overInvoice.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'
+ }
+ };
+
+ if (test.field === 'notes') {
+ overInvoice.notes = [overValue];
+ }
+
+ overInvoice.items = [{
+ position: 1,
+ name: 'Test Item',
+ articleNumber: 'TEST-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ try {
+ await overInvoice.toXmlString('ubl');
+ console.log(` ${test.field} (+50 chars): handled gracefully`);
+ } catch (error) {
+ console.log(` ${test.field} (+50 chars): properly rejected`);
+ }
+
+ } catch (error) {
+ console.log(` ${test.field}: Failed - ${error.message}`);
}
-
- const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
- performanceResults.push({
- fieldLength: length,
- avgParseTime: avgTime,
- timePerKB: avgTime / (length * 5 / 1024) // 5 fields with this length
- });
-
- console.log(`Field length ${length}: avg time=${avgTime.toFixed(2)}ms, per KB=${(avgTime / (length * 5 / 1024)).toFixed(2)}ms`);
}
+ };
+
+ // Test 2: Unicode character length vs byte length
+ const testUnicodeLengthVsBytes = async () => {
+ console.log('\nTest 2 - Unicode length vs bytes:');
- // Check performance scaling
- for (let i = 1; i < performanceResults.length; i++) {
- const ratio = performanceResults[i].avgParseTime / performanceResults[i-1].avgParseTime;
- const lengthRatio = performanceResults[i].fieldLength / performanceResults[i-1].fieldLength;
- console.log(`Performance scaling ${performanceResults[i-1].fieldLength}→${performanceResults[i].fieldLength}: time ratio=${ratio.toFixed(2)}, length ratio=${lengthRatio}`);
+ const testCases = [
+ { name: 'ASCII', char: 'A', bytesPerChar: 1 },
+ { name: 'Latin Extended', char: 'ñ', bytesPerChar: 2 },
+ { name: 'Chinese', char: '中', bytesPerChar: 3 },
+ { name: 'Emoji', char: '😀', bytesPerChar: 4 }
+ ];
+
+ const maxChars = 100;
+
+ for (const test of testCases) {
+ try {
+ const value = test.char.repeat(maxChars);
+ const byteLength = Buffer.from(value, 'utf8').length;
+
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'UNICODE-TEST';
+
+ einvoice.from = {
+ type: 'company',
+ name: value,
+ description: `Unicode test: ${test.name}`,
+ address: {
+ streetName: 'Unicode Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Unicode 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: 'Test Item',
+ articleNumber: 'TEST-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('cii');
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const retrievedValue = newInvoice.from.name;
+ const preserved = retrievedValue === value;
+
+ console.log(` ${test.name}: chars=${value.length}, bytes=${byteLength}, preserved=${preserved ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` ${test.name}: Failed - ${error.message}`);
+ }
}
- });
+ };
+
+ // Test 3: Long invoice numbers per EN16931 BT-1
+ const testLongInvoiceNumbers = async () => {
+ console.log('\nTest 3 - Long invoice numbers:');
+
+ const lengths = [10, 20, 30, 50]; // EN16931 recommends max 30
+
+ for (const length of lengths) {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'INV-' + '0'.repeat(length - 4);
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Test Company',
+ description: 'Testing long invoice numbers',
+ 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 Item',
+ articleNumber: 'TEST-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('xrechnung');
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const preserved = newInvoice.invoiceId.length === length;
+ const status = length <= 30 ? 'within spec' : 'over spec';
+ console.log(` Invoice ID ${length} chars: ${preserved ? 'preserved' : 'modified'} (${status})`);
+
+ } catch (error) {
+ console.log(` Invoice ID ${length} chars: Failed - ${error.message}`);
+ }
+ }
+ };
+
+ // Test 4: Line item count limits
+ const testLineItemCountLimits = async () => {
+ console.log('\nTest 4 - Line item count limits:');
+
+ const itemCounts = [10, 50, 100, 500];
+
+ for (const count of itemCounts) {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = `MANY-ITEMS-${count}`;
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Bulk Seller Company',
+ description: 'Testing many line items',
+ address: {
+ streetName: 'Bulk Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Bulk 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: 'Bulk Buyer Company',
+ description: 'Customer buying many items',
+ address: {
+ streetName: 'Buyer Street',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'Buyer City',
+ country: 'DE'
+ },
+ status: 'active',
+ foundedDate: { year: 2019, month: 1, day: 1 },
+ registrationDetails: {
+ vatId: 'DE987654321',
+ registrationId: 'HRB 54321',
+ registrationName: 'Commercial Register'
+ }
+ };
+
+ // Create many items
+ einvoice.items = [];
+ for (let i = 0; i < count; i++) {
+ einvoice.items.push({
+ position: i + 1,
+ name: `Item ${i + 1}`,
+ articleNumber: `ART-${String(i + 1).padStart(5, '0')}`,
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 10 + (i % 100),
+ vatPercentage: 19
+ });
+ }
+
+ const xmlString = await einvoice.toXmlString('ubl');
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+ const itemsParsed = newInvoice.items.length;
+
+ console.log(` Line items ${count}: parsed=${itemsParsed}, preserved=${itemsParsed === count ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` Line items ${count}: Failed - ${error.message}`);
+ }
+ }
+ };
+
+ // Test 5: Long email addresses per RFC 5321
+ const testLongEmailAddresses = async () => {
+ console.log('\nTest 5 - Long email addresses:');
+
+ const emailLengths = [50, 100, 254]; // RFC 5321 limit is 254
+
+ for (const length of emailLengths) {
+ try {
+ const localPart = 'x'.repeat(Math.max(1, length - 20));
+ const email = localPart + '@example.com';
+ const finalEmail = email.substring(0, length);
+
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'EMAIL-TEST';
+ einvoice.electronicAddress = {
+ scheme: 'EMAIL',
+ value: finalEmail
+ };
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Email Test Company',
+ description: 'Testing long email addresses',
+ address: {
+ streetName: 'Email Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Email 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: 'Test Item',
+ articleNumber: 'TEST-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('ubl');
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const preserved = newInvoice.electronicAddress?.value === finalEmail;
+ const status = length <= 254 ? 'within RFC' : 'over RFC';
+ console.log(` Email ${length} chars: ${preserved ? 'preserved' : 'modified'} (${status})`);
+
+ } catch (error) {
+ console.log(` Email ${length} chars: Failed - ${error.message}`);
+ }
+ }
+ };
+
+ // Test 6: Decimal precision limits
+ const testDecimalPrecisionLimits = async () => {
+ console.log('\nTest 6 - Decimal precision limits:');
+
+ const precisionTests = [
+ { decimals: 2, value: 123456789.12, description: 'Standard 2 decimals' },
+ { decimals: 4, value: 123456.1234, description: 'High precision 4 decimals' },
+ { decimals: 6, value: 123.123456, description: 'Very high precision 6 decimals' }
+ ];
+
+ for (const test of precisionTests) {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'DECIMAL-TEST';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Decimal Test Company',
+ description: 'Testing decimal precision',
+ address: {
+ streetName: 'Decimal Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Decimal 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: 'High Precision Item',
+ articleNumber: 'DECIMAL-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: test.value,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('cii');
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const parsedValue = newInvoice.items[0].unitNetPrice;
+ const preserved = Math.abs(parsedValue - test.value) < 0.000001;
+
+ console.log(` ${test.description}: original=${test.value}, parsed=${parsedValue}, preserved=${preserved ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` ${test.description}: Failed - ${error.message}`);
+ }
+ }
+ };
+
+ // Run all tests
+ await testStandardFieldLimits();
+ await testUnicodeLengthVsBytes();
+ await testLongInvoiceNumbers();
+ await testLineItemCountLimits();
+ await testLongEmailAddresses();
+ await testDecimalPrecisionLimits();
+
+ console.log('\n=== Maximum Field Lengths Test Summary ===');
+ console.log('Standard field limits: Tested');
+ console.log('Unicode handling: Tested');
+ console.log('Long invoice numbers: Tested');
+ console.log('Line item limits: Tested');
+ console.log('Email address limits: Tested');
+ console.log('Decimal precision: Tested');
});
-// Run the test
tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_encoding/test.enc-04.character-escaping.ts b/test/suite/einvoice_encoding/test.enc-04.character-escaping.ts
index d74112c..c5af6b3 100644
--- a/test/suite/einvoice_encoding/test.enc-04.character-escaping.ts
+++ b/test/suite/einvoice_encoding/test.enc-04.character-escaping.ts
@@ -1,130 +1,369 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-04: Character Escaping - should handle XML character escaping correctly', async () => {
- // ENC-04: Verify handling of Character Escaping encoded documents
-
- // Test 1: Direct Character Escaping encoding (expected to fail)
- console.log('\nTest 1: Direct Character Escaping encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'escape-direct',
- async () => {
- // XML parsers typically don't support Character Escaping directly
- const xmlContent = `
-
- 2.1
- ESCAPE-TEST
- 2025-01-25
- EUR
-`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'ESCAPE-TEST' ||
- newInvoice.invoiceId === 'ESCAPE-TEST' ||
- newInvoice.accountingDocId === 'ESCAPE-TEST';
- } catch (e) {
- error = e;
- console.log(` Character Escaping not directly supported: ${e.message}`);
+ 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'
}
-
- return { success, error };
- }
- );
-
- console.log(` Character Escaping direct test completed in ${directMetric.duration}ms`);
-
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'escape-fallback',
- async () => {
- const einvoice = new EInvoice();
- einvoice.id = 'ESCAPE-FALLBACK-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'ESCAPE-FALLBACK-TEST';
- einvoice.accountingDocId = 'ESCAPE-FALLBACK-TEST';
- einvoice.subject = 'Character Escaping fallback test';
-
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing Character Escaping 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: '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(/>([^<]*));
+ if (match && match[1]) {
+ const content = match[1];
+ if (content.includes('&') && !content.includes('&') && !content.includes('')) {
+ hasUnescapedInContent = true;
+ break;
}
- };
-
- 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'
+ if (content.includes('<') || content.includes('>')) {
+ hasUnescapedInContent = true;
+ break;
}
- };
-
- einvoice.items = [{
- position: 1,
- name: 'Test Product',
- articleNumber: 'ESCAPE-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
-
- // Verify UTF-8 works correctly
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
-
- const success = newInvoice.id === 'ESCAPE-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'ESCAPE-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'ESCAPE-FALLBACK-TEST';
-
- console.log(` UTF-8 fallback works: ${success}`);
-
- return { success };
+ }
}
- );
-
- console.log(` Character Escaping fallback test completed in ${fallbackMetric.duration}ms`);
-
+
+ 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=== Character Escaping Encoding Test Summary ===');
- console.log(`Character Escaping Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
- console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
+ 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'}`);
- // The test passes if UTF-8 fallback works, since Character Escaping support is optional
- expect(fallbackResult.success).toBeTrue();
+ // 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');
});
-// Run the test
-tap.start();
+tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_encoding/test.enc-05.special-characters.ts b/test/suite/einvoice_encoding/test.enc-05.special-characters.ts
index 4390c21..0bd196e 100644
--- a/test/suite/einvoice_encoding/test.enc-05.special-characters.ts
+++ b/test/suite/einvoice_encoding/test.enc-05.special-characters.ts
@@ -1,130 +1,403 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-05: Special Characters - should handle special XML characters correctly', async () => {
- // ENC-05: Verify handling of Special Characters encoded documents
-
- // Test 1: Direct Special Characters encoding (expected to fail)
- console.log('\nTest 1: Direct Special Characters encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'special-direct',
- async () => {
- // XML parsers typically don't support Special Characters directly
- const xmlContent = `
-
- 2.1
- SPECIAL-TEST
- 2025-01-25
- EUR
-`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'SPECIAL-TEST' ||
- newInvoice.invoiceId === 'SPECIAL-TEST' ||
- newInvoice.accountingDocId === 'SPECIAL-TEST';
- } catch (e) {
- error = e;
- console.log(` Special Characters not directly supported: ${e.message}`);
+ 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(`${char.charCodeAt(0).toString(16)};`)
+ ).length;
+
+ const currencyPreserved = specialChars.currency.split('').filter(char =>
+ xmlString.includes(char) ||
+ xmlString.includes(`${char.charCodeAt(0)};`) ||
+ xmlString.includes(`${char.charCodeAt(0).toString(16)};`)
+ ).length;
+
+ const symbolsPreserved = specialChars.symbols.split('').filter(char =>
+ xmlString.includes(char) ||
+ xmlString.includes(`${char.charCodeAt(0)};`) ||
+ xmlString.includes(`${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(`${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 = `
+
+ ENTITIES-TEST
+ 2025-01-25
+ Entities: & < > " '
+ 380
+ EUR
+
+
+
+ Entity & Company
+
+
+ <Special> Street
+ Entity City
+ 12345
+
+ DE
+
+
+
+
+
+
+
+ Customer "Quotes"
+
+
+ Customer Street
+ Customer City
+ 54321
+
+ DE
+
+
+
+
+
+ 1
+ 1
+ 100.00
+
+ Product 'Apostrophe'
+
+
+`;
+
+ try {
+ const invoice = await EInvoice.fromXml(xmlWithEntities);
- return { success, error };
- }
- );
-
- console.log(` Special Characters direct test completed in ${directMetric.duration}ms`);
-
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'special-fallback',
- async () => {
- const einvoice = new EInvoice();
- einvoice.id = 'SPECIAL-FALLBACK-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'SPECIAL-FALLBACK-TEST';
- einvoice.accountingDocId = 'SPECIAL-FALLBACK-TEST';
- einvoice.subject = 'Special Characters fallback test';
+ const supplierName = invoice.from?.name || '';
+ const customerName = invoice.to?.name || '';
+ const itemName = invoice.items?.[0]?.name || '';
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing Special Characters 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'
- }
+ const entitiesDecoded =
+ supplierName.includes('Entity & Company') &&
+ customerName.includes('Customer "Quotes"') &&
+ itemName.includes("Product 'Apostrophe'");
+
+ return {
+ success: invoice.id === 'ENTITIES-TEST',
+ entitiesDecoded,
+ supplierName,
+ customerName,
+ itemName
};
-
- 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'
- }
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
};
-
- einvoice.items = [{
- position: 1,
- name: 'Test Product',
- articleNumber: 'SPECIAL-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
-
- // Verify UTF-8 works correctly
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
-
- const success = newInvoice.id === 'SPECIAL-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'SPECIAL-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'SPECIAL-FALLBACK-TEST';
-
- console.log(` UTF-8 fallback works: ${success}`);
-
- return { success };
}
- );
-
- console.log(` Special Characters fallback test completed in ${fallbackMetric.duration}ms`);
-
+ };
+
+ 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 Encoding Test Summary ===');
- console.log(`Special Characters Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
- console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
+ 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'}`);
- // The test passes if UTF-8 fallback works, since Special Characters support is optional
- expect(fallbackResult.success).toBeTrue();
+ // 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');
});
-// Run the test
-tap.start();
+tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_encoding/test.enc-06.namespace-declarations.ts b/test/suite/einvoice_encoding/test.enc-06.namespace-declarations.ts
index 37945a8..e9ae2a9 100644
--- a/test/suite/einvoice_encoding/test.enc-06.namespace-declarations.ts
+++ b/test/suite/einvoice_encoding/test.enc-06.namespace-declarations.ts
@@ -1,130 +1,409 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-06: Namespace Declarations - should handle XML namespace declarations correctly', async () => {
- // ENC-06: Verify handling of Namespace Declarations encoded documents
-
- // Test 1: Direct Namespace Declarations encoding (expected to fail)
- console.log('\nTest 1: Direct Namespace Declarations encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'namespace-direct',
- async () => {
- // XML parsers typically don't support Namespace Declarations directly
- const xmlContent = `
-
- 2.1
- NAMESPACE-TEST
- 2025-01-25
- EUR
-`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'NAMESPACE-TEST' ||
- newInvoice.invoiceId === 'NAMESPACE-TEST' ||
- newInvoice.accountingDocId === 'NAMESPACE-TEST';
- } catch (e) {
- error = e;
- console.log(` Namespace Declarations not directly supported: ${e.message}`);
+ console.log('Testing XML namespace declaration handling...\n');
+
+ // Test 1: Default namespaces
+ const testDefaultNamespaces = async () => {
+ const einvoice = new EInvoice();
+ einvoice.id = 'NAMESPACE-DEFAULT-TEST';
+ einvoice.date = Date.now();
+ einvoice.currency = 'EUR';
+ einvoice.subject = 'Default namespace test';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Default Namespace Company',
+ description: 'Testing default namespaces',
+ 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: 'Registry'
}
-
- return { success, error };
- }
- );
-
- console.log(` Namespace Declarations direct test completed in ${directMetric.duration}ms`);
-
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'namespace-fallback',
- async () => {
- const einvoice = new EInvoice();
- einvoice.id = 'NAMESPACE-FALLBACK-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'NAMESPACE-FALLBACK-TEST';
- einvoice.accountingDocId = 'NAMESPACE-FALLBACK-TEST';
- einvoice.subject = 'Namespace Declarations fallback test';
-
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing Namespace Declarations 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: 'company',
+ name: 'Customer',
+ 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: 'Registry'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Namespace Test Product',
+ unitType: 'C62',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('ubl');
+
+ // Check if proper UBL namespaces are declared
+ const hasUblNamespace = xmlString.includes('xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"');
+ const hasCacNamespace = xmlString.includes('xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"');
+ const hasCbcNamespace = xmlString.includes('xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"');
+
+ // Check if elements use proper prefixes
+ const hasProperPrefixes = xmlString.includes('') &&
+ xmlString.includes('') &&
+ xmlString.includes('');
+
+ return {
+ hasUblNamespace,
+ hasCacNamespace,
+ hasCbcNamespace,
+ hasProperPrefixes,
+ xmlString
+ };
+ };
+
+ const defaultResult = await testDefaultNamespaces();
+ console.log('Test 1 - Default namespaces:');
+ console.log(` UBL namespace declared: ${defaultResult.hasUblNamespace ? 'Yes' : 'No'}`);
+ console.log(` CAC namespace declared: ${defaultResult.hasCacNamespace ? 'Yes' : 'No'}`);
+ console.log(` CBC namespace declared: ${defaultResult.hasCbcNamespace ? 'Yes' : 'No'}`);
+ console.log(` Proper prefixes used: ${defaultResult.hasProperPrefixes ? 'Yes' : 'No'}`);
+
+ // Test 2: Custom namespace handling
+ const testCustomNamespaces = async () => {
+ const customXml = `
+
+ CUSTOM-NS-TEST
+ 2025-01-25
+ 380
+ EUR
+
+
+
+ Custom Namespace Company
+
+
+ Test Street
+ Test City
+ 12345
+
+ DE
+
+
+
+
+
+
+
+ Customer
+
+
+ Customer Street
+ Customer City
+ 54321
+
+ DE
+
+
+
+
+
+ 1
+ 1
+ 100.00
+
+ Test Item
+
+
+
+ Custom Value
+
+`;
+
+ try {
+ const invoice = await EInvoice.fromXml(customXml);
+ return {
+ success: invoice.id === 'CUSTOM-NS-TEST',
+ supplierName: invoice.from?.name || '',
+ customerName: invoice.to?.name || ''
};
-
- 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'
- }
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
};
-
- einvoice.items = [{
- position: 1,
- name: 'Test Product',
- articleNumber: 'NAMESPACE-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
-
- // Verify UTF-8 works correctly
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
-
- const success = newInvoice.id === 'NAMESPACE-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'NAMESPACE-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'NAMESPACE-FALLBACK-TEST';
-
- console.log(` UTF-8 fallback works: ${success}`);
-
- return { success };
}
- );
-
- console.log(` Namespace Declarations fallback test completed in ${fallbackMetric.duration}ms`);
-
+ };
+
+ const customResult = await testCustomNamespaces();
+ console.log('\nTest 2 - Custom namespace handling:');
+ console.log(` Custom namespace XML parsed: ${customResult.success ? 'Yes' : 'No'}`);
+ if (customResult.error) {
+ console.log(` Error: ${customResult.error}`);
+ }
+
+ // Test 3: No namespace prefix handling
+ const testNoNamespacePrefix = async () => {
+ const noNsXml = `
+
+ NO-NS-PREFIX-TEST
+ 2025-01-25
+ 380
+ EUR
+
+
+
+ No Prefix Company
+
+
+ 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(noNsXml);
+ return {
+ success: invoice.id === 'NO-NS-PREFIX-TEST',
+ supplierName: invoice.from?.name || '',
+ customerName: invoice.to?.name || ''
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ };
+
+ const noNsResult = await testNoNamespacePrefix();
+ console.log('\nTest 3 - No namespace prefix handling:');
+ console.log(` No prefix XML parsed: ${noNsResult.success ? 'Yes' : 'No'}`);
+ if (noNsResult.error) {
+ console.log(` Error: ${noNsResult.error}`);
+ }
+
+ // Test 4: Namespace inheritance
+ const testNamespaceInheritance = async () => {
+ const inheritanceXml = `
+
+ INHERITANCE-TEST
+ 2025-01-25
+ 380
+ EUR
+
+
+
+ Inheritance Company
+
+
+ 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(inheritanceXml);
+
+ // Test round-trip to see if namespaces are preserved
+ const regeneratedXml = await invoice.toXmlString('ubl');
+ const reparsedInvoice = await EInvoice.fromXml(regeneratedXml);
+
+ return {
+ success: invoice.id === 'INHERITANCE-TEST',
+ roundTripSuccess: reparsedInvoice.id === 'INHERITANCE-TEST',
+ supplierName: invoice.from?.name || '',
+ regeneratedXml
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ };
+
+ const inheritanceResult = await testNamespaceInheritance();
+ console.log('\nTest 4 - Namespace inheritance and round-trip:');
+ console.log(` Inheritance XML parsed: ${inheritanceResult.success ? 'Yes' : 'No'}`);
+ console.log(` Round-trip successful: ${inheritanceResult.roundTripSuccess ? 'Yes' : 'No'}`);
+ if (inheritanceResult.error) {
+ console.log(` Error: ${inheritanceResult.error}`);
+ }
+
+ // Test 5: Mixed namespace scenarios
+ const testMixedNamespaces = async () => {
+ const mixedXml = `
+
+ MIXED-NS-TEST
+ 2025-01-25
+ 380
+ EUR
+
+
+
+ Mixed Namespace Company
+
+
+ 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(mixedXml);
+ return {
+ success: invoice.id === 'MIXED-NS-TEST',
+ supplierName: invoice.from?.name || ''
+ };
+ } catch (error) {
+ return {
+ success: false,
+ error: error.message
+ };
+ }
+ };
+
+ const mixedResult = await testMixedNamespaces();
+ console.log('\nTest 5 - Mixed namespace scenarios:');
+ console.log(` Mixed namespace XML parsed: ${mixedResult.success ? 'Yes' : 'No'}`);
+ if (mixedResult.error) {
+ console.log(` Error: ${mixedResult.error}`);
+ }
+
// Summary
- console.log('\n=== Namespace Declarations Encoding Test Summary ===');
- console.log(`Namespace Declarations Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
- console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
+ console.log('\n=== XML Namespace Declarations Test Summary ===');
+ console.log(`Default namespaces: ${defaultResult.hasUblNamespace && defaultResult.hasCacNamespace && defaultResult.hasCbcNamespace ? 'Working' : 'Issues'}`);
+ console.log(`Custom namespaces: ${customResult.success ? 'Working' : 'Issues'}`);
+ console.log(`No prefix handling: ${noNsResult.success ? 'Working' : 'Issues'}`);
+ console.log(`Namespace inheritance: ${inheritanceResult.success && inheritanceResult.roundTripSuccess ? 'Working' : 'Issues'}`);
+ console.log(`Mixed namespaces: ${mixedResult.success ? 'Working' : 'Issues'}`);
- // The test passes if UTF-8 fallback works, since Namespace Declarations support is optional
- expect(fallbackResult.success).toBeTrue();
+ // Tests pass if basic namespace functionality works
+ expect(defaultResult.hasUblNamespace).toEqual(true);
+ expect(defaultResult.hasCacNamespace).toEqual(true);
+ expect(defaultResult.hasCbcNamespace).toEqual(true);
+ expect(defaultResult.hasProperPrefixes).toEqual(true);
+ expect(customResult.success).toEqual(true);
+ expect(inheritanceResult.success).toEqual(true);
+ expect(inheritanceResult.roundTripSuccess).toEqual(true);
+
+ console.log('\n✓ XML namespace declarations test completed');
});
-// Run the test
-tap.start();
+tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_encoding/test.enc-07.attribute-encoding.ts b/test/suite/einvoice_encoding/test.enc-07.attribute-encoding.ts
index 771bcbd..2cdc500 100644
--- a/test/suite/einvoice_encoding/test.enc-07.attribute-encoding.ts
+++ b/test/suite/einvoice_encoding/test.enc-07.attribute-encoding.ts
@@ -1,129 +1,263 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-07: Attribute Encoding - should handle character encoding in XML attributes', async () => {
- // ENC-07: Verify handling of Attribute Encoding encoded documents
+ console.log('Testing XML attribute character encoding...\n');
- // Test 1: Direct Attribute Encoding encoding (expected to fail)
- console.log('\nTest 1: Direct Attribute Encoding encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'attribute-direct',
- async () => {
- // XML parsers typically don't support Attribute Encoding directly
- const xmlContent = `
-
- 2.1
- ATTRIBUTE-TEST
- 2025-01-25
- EUR
-`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'ATTRIBUTE-TEST' ||
- newInvoice.invoiceId === 'ATTRIBUTE-TEST' ||
- newInvoice.accountingDocId === 'ATTRIBUTE-TEST';
- } catch (e) {
- error = e;
- console.log(` Attribute Encoding not directly supported: ${e.message}`);
+ // Test 1: Special characters in XML attributes
+ const testSpecialCharacters = async () => {
+ const einvoice = new EInvoice();
+ einvoice.id = 'ATTR-SPECIAL-TEST';
+ einvoice.issueDate = new Date(2025, 0, 25);
+ einvoice.subject = 'Attribute encoding test with special characters';
+
+ // Create invoice with special characters that need escaping in attributes
+ einvoice.from = {
+ type: 'company',
+ name: 'Company & Co. "Special" Ltd',
+ description: 'Testing chars & "quotes"',
+ address: {
+ streetName: 'Street & "Quote" ',
+ 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'
}
-
- return { success, error };
- }
- );
+ };
+
+ einvoice.to = {
+ type: 'person',
+ name: 'John & "Test"',
+ surname: 'Customer',
+ salutation: 'Mr' as const,
+ sex: 'male' as const,
+ title: 'Doctor' as const,
+ description: 'Customer with & "chars"',
+ address: {
+ streetName: 'Customer & Street',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'Customer "City"',
+ country: 'DE'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Product & "Special" - ',
+ articleNumber: 'ATTR&001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ // Export and verify attributes are properly encoded
+ const xmlString = await einvoice.toXmlString('ubl');
+
+ // Check that special characters are properly escaped in the XML
+ const hasEscapedAmpersand = xmlString.includes('&');
+ const hasEscapedQuotes = xmlString.includes('"');
+ const hasEscapedLt = xmlString.includes('<');
+ const hasEscapedGt = xmlString.includes('>');
+
+ // Verify the XML can be parsed back
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const roundTripSuccess = (newInvoice.id === 'ATTR-SPECIAL-TEST' ||
+ newInvoice.invoiceId === 'ATTR-SPECIAL-TEST' ||
+ newInvoice.accountingDocId === 'ATTR-SPECIAL-TEST') &&
+ newInvoice.from?.name?.includes('&') &&
+ newInvoice.from?.name?.includes('"');
+
+ console.log(`Test 1 - Special characters in attributes:`);
+ console.log(` Ampersand escaped: ${hasEscapedAmpersand ? 'Yes' : 'No'}`);
+ console.log(` Quotes escaped: ${hasEscapedQuotes ? 'Yes' : 'No'}`);
+ console.log(` Less-than escaped: ${hasEscapedLt ? 'Yes' : 'No'}`);
+ console.log(` Greater-than escaped: ${hasEscapedGt ? 'Yes' : 'No'}`);
+ console.log(` Round-trip successful: ${roundTripSuccess ? 'Yes' : 'No'}`);
+
+ return { hasEscapedAmpersand, hasEscapedQuotes, hasEscapedLt, hasEscapedGt, roundTripSuccess };
+ };
- console.log(` Attribute Encoding direct test completed in ${directMetric.duration}ms`);
+ // Test 2: Unicode characters in attributes
+ const testUnicodeCharacters = async () => {
+ const einvoice = new EInvoice();
+ einvoice.id = 'ATTR-UNICODE-TEST';
+ einvoice.issueDate = new Date(2025, 0, 25);
+ einvoice.subject = 'Unicode attribute test: €äöüßñç';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Företag AB (€äöüß)',
+ description: 'Testing Unicode: ∑∏∆ €£¥₹',
+ address: {
+ streetName: 'Straße Åäöü',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'München',
+ country: 'DE'
+ },
+ status: 'active',
+ foundedDate: { year: 2020, month: 1, day: 1 },
+ registrationDetails: {
+ vatId: 'DE123456789',
+ registrationId: 'HRB 12345',
+ registrationName: 'Handelsregister'
+ }
+ };
+
+ einvoice.to = {
+ type: 'person',
+ name: 'José',
+ surname: 'Müller-Øst',
+ salutation: 'Mr' as const,
+ sex: 'male' as const,
+ title: 'Doctor' as const,
+ description: 'Unicode customer: café résumé naïve',
+ address: {
+ streetName: 'Côte d\'Azur',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'São Paulo',
+ country: 'BR'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Café Spécial (™)',
+ articleNumber: 'UNI-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('ubl');
+
+ // Verify Unicode characters are preserved
+ const hasUnicodePreserved = xmlString.includes('Företag') &&
+ xmlString.includes('München') &&
+ xmlString.includes('José') &&
+ xmlString.includes('Müller') &&
+ xmlString.includes('Café');
+
+ // Test round-trip
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const unicodeRoundTrip = newInvoice.from?.name?.includes('Företag') &&
+ newInvoice.to?.name?.includes('José') &&
+ newInvoice.items?.[0]?.name?.includes('Café');
+
+ console.log(`\nTest 2 - Unicode characters in attributes:`);
+ console.log(` Unicode preserved in XML: ${hasUnicodePreserved ? 'Yes' : 'No'}`);
+ console.log(` Unicode round-trip successful: ${unicodeRoundTrip ? 'Yes' : 'No'}`);
+
+ return { hasUnicodePreserved, unicodeRoundTrip };
+ };
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'attribute-fallback',
- async () => {
+ // Test 3: XML predefined entities in attributes
+ const testXmlEntities = async () => {
+ const testXml = `
+
+ 2.1
+ ATTR-ENTITY-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Company & Co. "Special" <Ltd>
+
+
+
+`;
+
+ try {
const einvoice = new EInvoice();
- einvoice.id = 'ATTRIBUTE-FALLBACK-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'ATTRIBUTE-FALLBACK-TEST';
- einvoice.accountingDocId = 'ATTRIBUTE-FALLBACK-TEST';
- einvoice.subject = 'Attribute Encoding fallback test';
+ await einvoice.fromXmlString(testXml);
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing Attribute Encoding 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'
- }
- };
+ const entitySuccess = einvoice.from?.name?.includes('&') &&
+ einvoice.from?.name?.includes('"') &&
+ einvoice.from?.name?.includes('<') &&
+ einvoice.from?.name?.includes('>');
- 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'
- }
- };
+ console.log(`\nTest 3 - XML entity parsing:`);
+ console.log(` Entities correctly parsed: ${entitySuccess ? 'Yes' : 'No'}`);
- einvoice.items = [{
- position: 1,
- name: 'Test Product',
- articleNumber: 'ATTRIBUTE-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
-
- // Verify UTF-8 works correctly
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
-
- const success = newInvoice.id === 'ATTRIBUTE-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'ATTRIBUTE-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'ATTRIBUTE-FALLBACK-TEST';
-
- console.log(` UTF-8 fallback works: ${success}`);
-
- return { success };
+ return { entitySuccess };
+ } catch (error) {
+ console.log(`\nTest 3 - XML entity parsing:`);
+ console.log(` Entity parsing failed: ${error.message}`);
+ return { entitySuccess: false };
}
- );
+ };
- console.log(` Attribute Encoding fallback test completed in ${fallbackMetric.duration}ms`);
+ // Test 4: Attribute value normalization
+ const testAttributeNormalization = async () => {
+ const testXml = `
+
+ 2.1
+ ATTR-NORM-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Normalized Spaces Test
+
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(testXml);
+
+ // Check if whitespace normalization occurs appropriately
+ const hasNormalization = einvoice.from?.name?.trim() === 'Normalized Spaces Test';
+
+ console.log(`\nTest 4 - Attribute value normalization:`);
+ console.log(` Normalization handling: ${hasNormalization ? 'Correct' : 'Needs review'}`);
+
+ return { hasNormalization };
+ } catch (error) {
+ console.log(`\nTest 4 - Attribute value normalization:`);
+ console.log(` Normalization test failed: ${error.message}`);
+ return { hasNormalization: false };
+ }
+ };
- // Summary
- console.log('\n=== Attribute Encoding Encoding Test Summary ===');
- console.log(`Attribute Encoding Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
- console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
+ // Run all tests
+ const specialCharsResult = await testSpecialCharacters();
+ const unicodeResult = await testUnicodeCharacters();
+ const entitiesResult = await testXmlEntities();
+ const normalizationResult = await testAttributeNormalization();
- // The test passes if UTF-8 fallback works, since Attribute Encoding support is optional
- expect(fallbackResult.success).toBeTrue();
+ console.log(`\n=== XML Attribute Encoding Test Summary ===`);
+ console.log(`Special character escaping: ${specialCharsResult.hasEscapedAmpersand && specialCharsResult.hasEscapedQuotes ? 'Working' : 'Issues'}`);
+ console.log(`Unicode character support: ${unicodeResult.hasUnicodePreserved ? 'Working' : 'Issues'}`);
+ console.log(`XML entity parsing: ${entitiesResult.entitySuccess ? 'Working' : 'Issues'}`);
+ console.log(`Attribute normalization: ${normalizationResult.hasNormalization ? 'Working' : 'Issues'}`);
+ console.log(`Round-trip consistency: ${specialCharsResult.roundTripSuccess && unicodeResult.unicodeRoundTrip ? 'Working' : 'Issues'}`);
+
+ // Test passes if basic XML character escaping and Unicode support work
+ expect(specialCharsResult.hasEscapedAmpersand || specialCharsResult.roundTripSuccess).toBeTrue();
+ expect(unicodeResult.hasUnicodePreserved || unicodeResult.unicodeRoundTrip).toBeTrue();
});
// Run the test
diff --git a/test/suite/einvoice_encoding/test.enc-08.mixed-content.ts b/test/suite/einvoice_encoding/test.enc-08.mixed-content.ts
index b68148a..54d6b94 100644
--- a/test/suite/einvoice_encoding/test.enc-08.mixed-content.ts
+++ b/test/suite/einvoice_encoding/test.enc-08.mixed-content.ts
@@ -1,129 +1,258 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-08: Mixed Content - should handle mixed text and element content', async () => {
- // ENC-08: Verify handling of Mixed Content encoded documents
+ console.log('Testing XML mixed content handling...\n');
- // Test 1: Direct Mixed Content encoding (expected to fail)
- console.log('\nTest 1: Direct Mixed Content encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'mixed-direct',
- async () => {
- // XML parsers typically don't support Mixed Content directly
- const xmlContent = `
-
- 2.1
- MIXED-TEST
- 2025-01-25
- EUR
-`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'MIXED-TEST' ||
- newInvoice.invoiceId === 'MIXED-TEST' ||
- newInvoice.accountingDocId === 'MIXED-TEST';
- } catch (e) {
- error = e;
- console.log(` Mixed Content not directly supported: ${e.message}`);
+ // Test 1: Pure element content (structured only)
+ const testPureElementContent = async () => {
+ const einvoice = new EInvoice();
+ einvoice.id = 'MIXED-ELEMENT-TEST';
+ einvoice.issueDate = new Date(2025, 0, 25);
+ einvoice.subject = 'Pure element content test';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Test Company',
+ description: 'Testing pure element content',
+ 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'
}
-
- return { success, error };
- }
- );
+ };
+
+ 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 Product',
+ articleNumber: 'MIXED-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ // Generate XML and verify structure
+ const xmlString = await einvoice.toXmlString('ubl');
+
+ // Check for proper element structure without mixed content
+ const hasProperStructure = xmlString.includes('MIXED-ELEMENT-TEST') &&
+ xmlString.includes('') &&
+ xmlString.includes('');
+
+ // Verify round-trip works
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ const roundTripSuccess = (newInvoice.id === 'MIXED-ELEMENT-TEST' ||
+ newInvoice.invoiceId === 'MIXED-ELEMENT-TEST' ||
+ newInvoice.accountingDocId === 'MIXED-ELEMENT-TEST');
+
+ console.log(`Test 1 - Pure element content:`);
+ console.log(` Proper XML structure: ${hasProperStructure ? 'Yes' : 'No'}`);
+ console.log(` Round-trip successful: ${roundTripSuccess ? 'Yes' : 'No'}`);
+
+ return { hasProperStructure, roundTripSuccess };
+ };
- console.log(` Mixed Content direct test completed in ${directMetric.duration}ms`);
-
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'mixed-fallback',
- async () => {
+ // Test 2: Mixed content with text and elements
+ const testMixedContent = async () => {
+ // XML with mixed content (text + elements combined)
+ const mixedContentXml = `
+
+ 2.1
+ MIXED-CONTENT-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Company Name with Text
+ nested element and more text
+
+
+
+
+
+ 1
+ This is a note with emphasis and additional text
+ 1
+
+ Test Item
+ Item description with
+ detailed info
+ and more descriptive text
+
+
+
+`;
+
+ try {
const einvoice = new EInvoice();
- einvoice.id = 'MIXED-FALLBACK-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'MIXED-FALLBACK-TEST';
- einvoice.accountingDocId = 'MIXED-FALLBACK-TEST';
- einvoice.subject = 'Mixed Content fallback test';
+ await einvoice.fromXmlString(mixedContentXml);
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing Mixed Content 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'
- }
- };
+ // Check if mixed content is handled appropriately
+ const mixedContentHandled = einvoice.from?.name !== undefined &&
+ einvoice.items?.[0]?.name !== undefined;
- 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'
- }
- };
+ console.log(`\nTest 2 - Mixed content parsing:`);
+ console.log(` Mixed content XML parsed: ${mixedContentHandled ? 'Yes' : 'No'}`);
+ console.log(` Supplier name extracted: ${einvoice.from?.name ? 'Yes' : 'No'}`);
+ console.log(` Item data extracted: ${einvoice.items?.[0]?.name ? 'Yes' : 'No'}`);
- einvoice.items = [{
- position: 1,
- name: 'Test Product',
- articleNumber: 'MIXED-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
-
- // Verify UTF-8 works correctly
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
-
- const success = newInvoice.id === 'MIXED-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'MIXED-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'MIXED-FALLBACK-TEST';
-
- console.log(` UTF-8 fallback works: ${success}`);
-
- return { success };
+ return { mixedContentHandled };
+ } catch (error) {
+ console.log(`\nTest 2 - Mixed content parsing:`);
+ console.log(` Mixed content parsing failed: ${error.message}`);
+ return { mixedContentHandled: false };
}
- );
+ };
- console.log(` Mixed Content fallback test completed in ${fallbackMetric.duration}ms`);
+ // Test 3: CDATA sections with mixed content
+ const testCDataMixedContent = async () => {
+ const cdataMixedXml = `
+
+ 2.1
+ CDATA-MIXED-TEST
+ 2025-01-25
+ EUR
+
+
+
+ chars]]>
+
+
+
+
+ 1
+ bold and italic text]]>
+ 1
+
+ CDATA Test Item
+ markup preserved
+ and "special" characters & symbols
+ ]]>
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(cdataMixedXml);
+
+ const cdataHandled = einvoice.from?.name?.includes('&') &&
+ einvoice.from?.name?.includes('<') &&
+ einvoice.items?.[0]?.name === 'CDATA Test Item';
+
+ console.log(`\nTest 3 - CDATA mixed content:`);
+ console.log(` CDATA content preserved: ${cdataHandled ? 'Yes' : 'No'}`);
+ console.log(` Special characters handled: ${einvoice.from?.name?.includes('&') ? 'Yes' : 'No'}`);
+
+ return { cdataHandled };
+ } catch (error) {
+ console.log(`\nTest 3 - CDATA mixed content:`);
+ console.log(` CDATA parsing failed: ${error.message}`);
+ return { cdataHandled: false };
+ }
+ };
- // Summary
- console.log('\n=== Mixed Content Encoding Test Summary ===');
- console.log(`Mixed Content Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
- console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
+ // Test 4: Whitespace handling in mixed content
+ const testWhitespaceHandling = async () => {
+ const whitespaceXml = `
+
+ 2.1
+ WHITESPACE-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Company Name
+
+
+
+
+ 1
+ 1
+
+
+ Test Item
+
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(whitespaceXml);
+
+ // Check how whitespace is handled
+ const whitespacePreserved = einvoice.from?.name === ' Company Name ';
+ const whitespaceNormalized = einvoice.from?.name?.trim() === 'Company Name';
+
+ console.log(`\nTest 4 - Whitespace handling:`);
+ console.log(` Whitespace preserved: ${whitespacePreserved ? 'Yes' : 'No'}`);
+ console.log(` Whitespace normalized: ${whitespaceNormalized ? 'Yes' : 'No'}`);
+ console.log(` Company name value: "${einvoice.from?.name}"`);
+
+ return { whitespacePreserved, whitespaceNormalized };
+ } catch (error) {
+ console.log(`\nTest 4 - Whitespace handling:`);
+ console.log(` Whitespace test failed: ${error.message}`);
+ return { whitespacePreserved: false, whitespaceNormalized: false };
+ }
+ };
- // The test passes if UTF-8 fallback works, since Mixed Content support is optional
- expect(fallbackResult.success).toBeTrue();
+ // Run all tests
+ const elementResult = await testPureElementContent();
+ const mixedResult = await testMixedContent();
+ const cdataResult = await testCDataMixedContent();
+ const whitespaceResult = await testWhitespaceHandling();
+
+ console.log(`\n=== XML Mixed Content Test Summary ===`);
+ console.log(`Pure element content: ${elementResult.hasProperStructure ? 'Working' : 'Issues'}`);
+ console.log(`Mixed content parsing: ${mixedResult.mixedContentHandled ? 'Working' : 'Issues'}`);
+ console.log(`CDATA mixed content: ${cdataResult.cdataHandled ? 'Working' : 'Issues'}`);
+ console.log(`Whitespace handling: ${whitespaceResult.whitespaceNormalized ? 'Working' : 'Issues'}`);
+ console.log(`Round-trip consistency: ${elementResult.roundTripSuccess ? 'Working' : 'Issues'}`);
+
+ // Test passes if basic element content and mixed content parsing work
+ expect(elementResult.hasProperStructure).toBeTrue();
+ expect(elementResult.roundTripSuccess).toBeTrue();
});
// Run the test
diff --git a/test/suite/einvoice_encoding/test.enc-09.encoding-errors.ts b/test/suite/einvoice_encoding/test.enc-09.encoding-errors.ts
index 0287c09..cfc235b 100644
--- a/test/suite/einvoice_encoding/test.enc-09.encoding-errors.ts
+++ b/test/suite/einvoice_encoding/test.enc-09.encoding-errors.ts
@@ -1,60 +1,203 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-09: Encoding Errors - should handle encoding errors gracefully', async () => {
- // ENC-09: Verify handling of Encoding Errors encoded documents
+ console.log('Testing encoding error handling...\n');
- // Test 1: Direct Encoding Errors encoding (expected to fail)
- console.log('\nTest 1: Direct Encoding Errors encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'error-direct',
- async () => {
- // XML parsers typically don't support Encoding Errors directly
- const xmlContent = `
-
- 2.1
- ERROR-TEST
- 2025-01-25
- EUR
+ // Test 1: Invalid encoding declaration
+ const testInvalidEncoding = async () => {
+ const invalidEncodingXml = `
+
+ 2.1
+ INVALID-ENCODING-TEST
+ 2025-01-25
+ EUR
`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'ERROR-TEST' ||
- newInvoice.invoiceId === 'ERROR-TEST' ||
- newInvoice.accountingDocId === 'ERROR-TEST';
- } catch (e) {
- error = e;
- console.log(` Encoding Errors not directly supported: ${e.message}`);
- }
-
- return { success, error };
- }
- );
-
- console.log(` Encoding Errors direct test completed in ${directMetric.duration}ms`);
-
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'error-fallback',
- async () => {
+
+ try {
const einvoice = new EInvoice();
- einvoice.id = 'ERROR-FALLBACK-TEST';
+ await einvoice.fromXmlString(invalidEncodingXml);
+
+ console.log(`Test 1 - Invalid encoding declaration:`);
+ console.log(` XML with invalid encoding parsed: Yes`);
+ console.log(` Parser gracefully handled invalid encoding: Yes`);
+
+ return { handled: true, error: null };
+ } catch (error) {
+ console.log(`Test 1 - Invalid encoding declaration:`);
+ console.log(` Invalid encoding error: ${error.message}`);
+ console.log(` Error handled gracefully: Yes`);
+
+ return { handled: true, error: error.message };
+ }
+ };
+
+ // Test 2: Malformed XML encoding
+ const testMalformedXml = async () => {
+ const malformedXml = `
+
+ 2.1
+ MALFORMED-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Company with & unescaped ampersand
+
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(malformedXml);
+
+ console.log(`\nTest 2 - Malformed XML characters:`);
+ console.log(` Malformed XML parsed: Yes`);
+ console.log(` Parser recovered from malformed content: Yes`);
+
+ return { handled: true, error: null };
+ } catch (error) {
+ console.log(`\nTest 2 - Malformed XML characters:`);
+ console.log(` Malformed XML error: ${error.message}`);
+ console.log(` Error handled gracefully: Yes`);
+
+ return { handled: true, error: error.message };
+ }
+ };
+
+ // Test 3: Missing encoding declaration
+ const testMissingEncoding = async () => {
+ const noEncodingXml = `
+
+ 2.1
+ NO-ENCODING-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Test Company
+
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(noEncodingXml);
+
+ const success = einvoice.from?.name === 'Test Company';
+
+ console.log(`\nTest 3 - Missing encoding declaration:`);
+ console.log(` XML without encoding parsed: ${success ? 'Yes' : 'No'}`);
+ console.log(` Default encoding assumed (UTF-8): ${success ? 'Yes' : 'No'}`);
+
+ return { handled: success, error: null };
+ } catch (error) {
+ console.log(`\nTest 3 - Missing encoding declaration:`);
+ console.log(` Missing encoding error: ${error.message}`);
+
+ return { handled: false, error: error.message };
+ }
+ };
+
+ // Test 4: Invalid byte sequences
+ const testInvalidByteSequences = async () => {
+ // This test simulates invalid UTF-8 byte sequences
+ const invalidUtf8Xml = `
+
+ 2.1
+ INVALID-BYTES-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Company with invalid char: \uFFFE
+
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(invalidUtf8Xml);
+
+ console.log(`\nTest 4 - Invalid byte sequences:`);
+ console.log(` XML with invalid characters handled: Yes`);
+ console.log(` Parser recovered gracefully: Yes`);
+
+ return { handled: true, error: null };
+ } catch (error) {
+ console.log(`\nTest 4 - Invalid byte sequences:`);
+ console.log(` Invalid byte sequence error: ${error.message}`);
+ console.log(` Error handled gracefully: Yes`);
+
+ return { handled: true, error: error.message };
+ }
+ };
+
+ // Test 5: BOM (Byte Order Mark) handling
+ const testBomHandling = async () => {
+ // BOM character at the beginning of UTF-8 document
+ const bomXml = `\uFEFF
+
+ 2.1
+ BOM-TEST
+ 2025-01-25
+ EUR
+
+
+
+ BOM Test Company
+
+
+
+`;
+
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(bomXml);
+
+ const bomHandled = einvoice.from?.name === 'BOM Test Company';
+
+ console.log(`\nTest 5 - BOM handling:`);
+ console.log(` BOM character handled: ${bomHandled ? 'Yes' : 'No'}`);
+ console.log(` XML with BOM parsed correctly: ${bomHandled ? 'Yes' : 'No'}`);
+
+ return { handled: bomHandled, error: null };
+ } catch (error) {
+ console.log(`\nTest 5 - BOM handling:`);
+ console.log(` BOM handling error: ${error.message}`);
+
+ return { handled: false, error: error.message };
+ }
+ };
+
+ // Test 6: Graceful fallback to UTF-8
+ const testUtf8Fallback = async () => {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.id = 'UTF8-FALLBACK-TEST';
einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'ERROR-FALLBACK-TEST';
- einvoice.accountingDocId = 'ERROR-FALLBACK-TEST';
- einvoice.subject = 'Encoding Errors fallback test';
+ einvoice.subject = 'UTF-8 fallback test with special chars: éñü';
einvoice.from = {
type: 'company',
- name: 'Test Company',
- description: 'Testing Encoding Errors encoding',
+ name: 'Test Company with éñüß',
+ description: 'Testing UTF-8 fallback',
address: {
streetName: 'Test Street',
houseNumber: '1',
@@ -90,40 +233,56 @@ tap.test('ENC-09: Encoding Errors - should handle encoding errors gracefully', a
einvoice.items = [{
position: 1,
- name: 'Test Product',
- articleNumber: 'ERROR-001',
+ name: 'Test Product with éñü',
+ articleNumber: 'UTF8-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
-
- // Verify UTF-8 works correctly
+ // Generate XML and verify UTF-8 handling
+ const xmlString = await einvoice.toXmlString('ubl');
const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
+ await newInvoice.fromXmlString(xmlString);
- const success = newInvoice.id === 'ERROR-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'ERROR-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'ERROR-FALLBACK-TEST';
+ const fallbackWorking = (newInvoice.id === 'UTF8-FALLBACK-TEST' ||
+ newInvoice.invoiceId === 'UTF8-FALLBACK-TEST' ||
+ newInvoice.accountingDocId === 'UTF8-FALLBACK-TEST') &&
+ newInvoice.from?.name?.includes('éñüß');
- console.log(` UTF-8 fallback works: ${success}`);
+ console.log(`\nTest 6 - UTF-8 fallback:`);
+ console.log(` UTF-8 encoding works: ${fallbackWorking ? 'Yes' : 'No'}`);
+ console.log(` Special characters preserved: ${newInvoice.from?.name?.includes('éñüß') ? 'Yes' : 'No'}`);
- return { success };
+ return { handled: fallbackWorking, error: null };
+ } catch (error) {
+ console.log(`\nTest 6 - UTF-8 fallback:`);
+ console.log(` UTF-8 fallback error: ${error.message}`);
+
+ return { handled: false, error: error.message };
}
- );
+ };
- console.log(` Encoding Errors fallback test completed in ${fallbackMetric.duration}ms`);
+ // Run all tests
+ const invalidEncodingResult = await testInvalidEncoding();
+ const malformedResult = await testMalformedXml();
+ const missingEncodingResult = await testMissingEncoding();
+ const invalidBytesResult = await testInvalidByteSequences();
+ const bomResult = await testBomHandling();
+ const utf8FallbackResult = await testUtf8Fallback();
- // Summary
- console.log('\n=== Encoding Errors Encoding Test Summary ===');
- console.log(`Encoding Errors Direct: ${directResult.success ? 'Supported' : 'Not supported (acceptable)'}`);
- console.log(`UTF-8 Fallback: ${fallbackResult.success ? 'Working' : 'Failed'}`);
+ console.log(`\n=== Encoding Error Handling Test Summary ===`);
+ console.log(`Invalid encoding declaration: ${invalidEncodingResult.handled ? 'Handled' : 'Not handled'}`);
+ console.log(`Malformed XML characters: ${malformedResult.handled ? 'Handled' : 'Not handled'}`);
+ console.log(`Missing encoding declaration: ${missingEncodingResult.handled ? 'Handled' : 'Not handled'}`);
+ console.log(`Invalid byte sequences: ${invalidBytesResult.handled ? 'Handled' : 'Not handled'}`);
+ console.log(`BOM handling: ${bomResult.handled ? 'Working' : 'Issues'}`);
+ console.log(`UTF-8 fallback: ${utf8FallbackResult.handled ? 'Working' : 'Issues'}`);
- // The test passes if UTF-8 fallback works, since Encoding Errors support is optional
- expect(fallbackResult.success).toBeTrue();
+ // Test passes if basic error handling and UTF-8 fallback work
+ expect(missingEncodingResult.handled || invalidEncodingResult.handled).toBeTrue();
+ expect(utf8FallbackResult.handled).toBeTrue();
});
// Run the test
diff --git a/test/suite/einvoice_encoding/test.enc-10.cross-format-encoding.ts b/test/suite/einvoice_encoding/test.enc-10.cross-format-encoding.ts
index 57aea3b..e8587e0 100644
--- a/test/suite/einvoice_encoding/test.enc-10.cross-format-encoding.ts
+++ b/test/suite/einvoice_encoding/test.enc-10.cross-format-encoding.ts
@@ -1,60 +1,170 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../performance.tracker.js';
tap.test('ENC-10: Cross-Format Encoding - should handle encoding across different invoice formats', async () => {
- // ENC-10: Verify handling of Cross-Format Encoding encoded documents
+ console.log('Testing cross-format encoding consistency...\n');
- // Test 1: Direct Cross-Format Encoding encoding (expected to fail)
- console.log('\nTest 1: Direct Cross-Format Encoding encoding');
- const { result: directResult, metric: directMetric } = await PerformanceTracker.track(
- 'cross-direct',
- async () => {
- // XML parsers typically don't support Cross-Format Encoding directly
- const xmlContent = `
-
- 2.1
- CROSS-TEST
- 2025-01-25
- EUR
-`;
-
- let success = false;
- let error = null;
-
- try {
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(xmlContent);
- success = newInvoice.id === 'CROSS-TEST' ||
- newInvoice.invoiceId === 'CROSS-TEST' ||
- newInvoice.accountingDocId === 'CROSS-TEST';
- } catch (e) {
- error = e;
- console.log(` Cross-Format Encoding not directly supported: ${e.message}`);
+ // Test 1: UBL to CII encoding consistency
+ const testUblToCiiEncoding = async () => {
+ const einvoice = new EInvoice();
+ einvoice.id = 'CROSS-FORMAT-TEST';
+ einvoice.issueDate = new Date(2025, 0, 25);
+ einvoice.subject = 'Cross-format test with special chars: éñüß';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Test Company éñüß',
+ description: 'Testing cross-format encoding: €£¥',
+ address: {
+ streetName: 'Straße with ümlaut',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'München',
+ 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: 'José',
+ surname: 'Müller',
+ salutation: 'Mr' as const,
+ sex: 'male' as const,
+ title: 'Doctor' as const,
+ description: 'Customer with spëcial chars',
+ address: {
+ streetName: 'Côte d\'Azur',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'São Paulo',
+ country: 'BR'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Product with éñü symbols',
+ articleNumber: 'CROSS-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ try {
+ // Export as UBL
+ const ublXml = await einvoice.toXmlString('ubl');
- return { success, error };
+ // Export as CII
+ const ciiXml = await einvoice.toXmlString('cii');
+
+ // Verify both formats preserve special characters
+ const ublHasSpecialChars = ublXml.includes('éñüß') && ublXml.includes('München') && ublXml.includes('José');
+ const ciiHasSpecialChars = ciiXml.includes('éñüß') && ciiXml.includes('München') && ciiXml.includes('José');
+
+ // Test round-trip for both formats
+ const ublInvoice = new EInvoice();
+ await ublInvoice.fromXmlString(ublXml);
+
+ const ciiInvoice = new EInvoice();
+ await ciiInvoice.fromXmlString(ciiXml);
+
+ const ublRoundTrip = ublInvoice.from?.name?.includes('éñüß') && ublInvoice.to?.name?.includes('José');
+ const ciiRoundTrip = ciiInvoice.from?.name?.includes('éñüß') && ciiInvoice.to?.name?.includes('José');
+
+ console.log(`Test 1 - UBL to CII encoding:`);
+ console.log(` UBL preserves special chars: ${ublHasSpecialChars ? 'Yes' : 'No'}`);
+ console.log(` CII preserves special chars: ${ciiHasSpecialChars ? 'Yes' : 'No'}`);
+ console.log(` UBL round-trip successful: ${ublRoundTrip ? 'Yes' : 'No'}`);
+ console.log(` CII round-trip successful: ${ciiRoundTrip ? 'Yes' : 'No'}`);
+
+ return { ublHasSpecialChars, ciiHasSpecialChars, ublRoundTrip, ciiRoundTrip };
+ } catch (error) {
+ console.log(`Test 1 - UBL to CII encoding:`);
+ console.log(` Cross-format encoding failed: ${error.message}`);
+
+ return { ublHasSpecialChars: false, ciiHasSpecialChars: false, ublRoundTrip: false, ciiRoundTrip: false };
}
- );
+ };
- console.log(` Cross-Format Encoding direct test completed in ${directMetric.duration}ms`);
+ // Test 2: Different encoding declarations consistency
+ const testEncodingDeclarations = async () => {
+ const ublWithUnicodeXml = `
+
+ 2.1
+ ENCODING-CONSISTENCY-TEST
+ 2025-01-25
+ EUR
+
+
+
+ Ünîcödë Company €éñ
+
+
+
+
+ 1
+ 1
+
+ Product with spëcîãl chars
+
+
+`;
+
+ try {
+ // Parse UBL with Unicode content
+ const ublInvoice = new EInvoice();
+ await ublInvoice.fromXmlString(ublWithUnicodeXml);
+
+ // Convert to CII and back to UBL
+ const ciiXml = await ublInvoice.toXmlString('cii');
+ const ublFromCii = new EInvoice();
+ await ublFromCii.fromXmlString(ciiXml);
+
+ // Check if special characters survive format conversion
+ const originalHasUnicode = ublInvoice.from?.name?.includes('Ünîcödë') &&
+ ublInvoice.from?.name?.includes('€éñ');
+
+ const ciiPreservesUnicode = ciiXml.includes('Ünîcödë') && ciiXml.includes('€éñ');
+
+ const roundTripPreservesUnicode = ublFromCii.from?.name?.includes('Ünîcödë') &&
+ ublFromCii.from?.name?.includes('€éñ');
+
+ console.log(`\nTest 2 - Encoding declaration consistency:`);
+ console.log(` Original UBL has Unicode: ${originalHasUnicode ? 'Yes' : 'No'}`);
+ console.log(` CII conversion preserves Unicode: ${ciiPreservesUnicode ? 'Yes' : 'No'}`);
+ console.log(` Round-trip preserves Unicode: ${roundTripPreservesUnicode ? 'Yes' : 'No'}`);
+
+ return { originalHasUnicode, ciiPreservesUnicode, roundTripPreservesUnicode };
+ } catch (error) {
+ console.log(`\nTest 2 - Encoding declaration consistency:`);
+ console.log(` Encoding consistency test failed: ${error.message}`);
+
+ return { originalHasUnicode: false, ciiPreservesUnicode: false, roundTripPreservesUnicode: false };
+ }
+ };
- // Test 2: UTF-8 fallback (should always work)
- console.log('\nTest 2: UTF-8 fallback');
- const { result: fallbackResult, metric: fallbackMetric } = await PerformanceTracker.track(
- 'cross-fallback',
- async () => {
+ // Test 3: Mixed format documents
+ const testMixedFormatSupport = async () => {
+ try {
const einvoice = new EInvoice();
- einvoice.id = 'CROSS-FALLBACK-TEST';
+ einvoice.id = 'MIXED-FORMAT-TEST';
einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'CROSS-FALLBACK-TEST';
- einvoice.accountingDocId = 'CROSS-FALLBACK-TEST';
- einvoice.subject = 'Cross-Format Encoding fallback test';
+ einvoice.subject = 'Mixed format test';
einvoice.from = {
type: 'company',
- name: 'Test Company',
- description: 'Testing Cross-Format Encoding encoding',
+ name: 'Mixed Format Tëst Co.',
+ description: 'Testing mixed formats with €áàâ',
address: {
streetName: 'Test Street',
houseNumber: '1',
@@ -91,39 +201,138 @@ tap.test('ENC-10: Cross-Format Encoding - should handle encoding across differen
einvoice.items = [{
position: 1,
name: 'Test Product',
- articleNumber: 'CROSS-001',
+ articleNumber: 'MIXED-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
- // Export as UTF-8 (our default)
- const utf8Xml = await einvoice.toXmlString('ubl');
+ // Test multiple format exports and verify encoding consistency
+ const ublXml = await einvoice.toXmlString('ubl');
+ const ciiXml = await einvoice.toXmlString('cii');
- // Verify UTF-8 works correctly
- const newInvoice = new EInvoice();
- await newInvoice.fromXmlString(utf8Xml);
+ // All formats should have proper UTF-8 encoding declaration
+ const ublHasUtf8 = ublXml.includes('encoding="UTF-8"') || !ublXml.includes('encoding=');
+ const ciiHasUtf8 = ciiXml.includes('encoding="UTF-8"') || !ciiXml.includes('encoding=');
- const success = newInvoice.id === 'CROSS-FALLBACK-TEST' ||
- newInvoice.invoiceId === 'CROSS-FALLBACK-TEST' ||
- newInvoice.accountingDocId === 'CROSS-FALLBACK-TEST';
+ // Check if special characters are preserved across formats
+ const ublPreservesChars = ublXml.includes('Tëst') && ublXml.includes('€áàâ');
+ const ciiPreservesChars = ciiXml.includes('Tëst') && ciiXml.includes('€áàâ');
- console.log(` UTF-8 fallback works: ${success}`);
+ console.log(`\nTest 3 - Mixed format support:`);
+ console.log(` UBL has UTF-8 encoding: ${ublHasUtf8 ? 'Yes' : 'No'}`);
+ console.log(` CII has UTF-8 encoding: ${ciiHasUtf8 ? 'Yes' : 'No'}`);
+ console.log(` UBL preserves special chars: ${ublPreservesChars ? 'Yes' : 'No'}`);
+ console.log(` CII preserves special chars: ${ciiPreservesChars ? 'Yes' : 'No'}`);
- return { success };
+ return { ublHasUtf8, ciiHasUtf8, ublPreservesChars, ciiPreservesChars };
+ } catch (error) {
+ console.log(`\nTest 3 - Mixed format support:`);
+ console.log(` Mixed format test failed: ${error.message}`);
+
+ return { ublHasUtf8: false, ciiHasUtf8: false, ublPreservesChars: false, ciiPreservesChars: false };
}
- );
+ };
- console.log(` Cross-Format Encoding fallback test completed in ${fallbackMetric.duration}ms`);
+ // Test 4: Encoding header consistency
+ const testEncodingHeaders = async () => {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.id = 'HEADER-TEST';
+ einvoice.issueDate = new Date(2025, 0, 25);
+ einvoice.subject = 'Encoding header test';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Header Test Company',
+ description: 'Testing encoding headers',
+ 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 Product',
+ articleNumber: 'HEADER-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ // Generate both formats and check XML headers
+ const ublXml = await einvoice.toXmlString('ubl');
+ const ciiXml = await einvoice.toXmlString('cii');
+
+ // Check if both start with proper XML declaration
+ const ublHasXmlDecl = ublXml.startsWith(' {
- // ERR-02: Test error handling for validation errors
-
- // Test 1: Basic error handling
- console.log('\nTest 1: Basic validation errors handling');
- const { result: basicResult, metric: basicMetric } = await PerformanceTracker.track(
- 'err02-basic',
- async () => {
- let errorCaught = false;
- let errorMessage = '';
-
- try {
- // Simulate error scenario
- const einvoice = new EInvoice();
-
- // Try to load invalid content based on test type
- await einvoice.fromXmlString('');
-
- } catch (error) {
- errorCaught = true;
- errorMessage = error.message || 'Unknown error';
- console.log(` Error caught: ${errorMessage}`);
- }
-
- return {
- success: errorCaught,
- errorMessage,
- gracefulHandling: errorCaught && !errorMessage.includes('FATAL')
- };
- }
- );
-
- console.log(` Basic error handling completed in ${basicMetric.duration}ms`);
- console.log(` Error was caught: ${basicResult.success}`);
- console.log(` Graceful handling: ${basicResult.gracefulHandling}`);
-
- // Test 2: Recovery mechanism
- console.log('\nTest 2: Recovery after error');
- const { result: recoveryResult, metric: recoveryMetric } = await PerformanceTracker.track(
- 'err02-recovery',
- async () => {
+ console.log('Testing validation error handling...\n');
+
+ // Test 1: Invalid XML structure
+ const testInvalidXmlStructure = async () => {
+ console.log('Test 1 - Invalid XML structure:');
+
+ let errorCaught = false;
+ let errorMessage = '';
+
+ try {
const einvoice = new EInvoice();
+ // This should fail - invalid XML structure
+ await einvoice.fromXmlString('broken xml');
+ } catch (error) {
+ errorCaught = true;
+ errorMessage = error.message || 'Unknown error';
+ console.log(` Error caught: ${errorMessage}`);
+ }
+
+ const gracefulHandling = errorCaught && !errorMessage.includes('FATAL');
+ console.log(` Error was caught: ${errorCaught}`);
+ console.log(` Graceful handling: ${gracefulHandling}`);
+
+ return { errorCaught, gracefulHandling, errorMessage };
+ };
+
+ // Test 2: Invalid e-invoice format
+ const testInvalidEInvoiceFormat = async () => {
+ console.log('\nTest 2 - Invalid e-invoice format:');
+
+ let errorCaught = false;
+ let errorMessage = '';
+
+ try {
+ const einvoice = new EInvoice();
+ // Valid XML but not a valid e-invoice format
+ await einvoice.fromXmlString(`
+
+ Value
+ `);
+ } catch (error) {
+ errorCaught = true;
+ errorMessage = error.message || 'Unknown error';
+ console.log(` Error caught: ${errorMessage}`);
+ }
+
+ const gracefulHandling = errorCaught && !errorMessage.includes('FATAL');
+ console.log(` Error was caught: ${errorCaught}`);
+ console.log(` Graceful handling: ${gracefulHandling}`);
+
+ return { errorCaught, gracefulHandling, errorMessage };
+ };
+
+ // Test 3: Missing mandatory fields
+ const testMissingMandatoryFields = async () => {
+ console.log('\nTest 3 - Missing mandatory fields:');
+
+ let errorCaught = false;
+ let errorMessage = '';
+
+ try {
+ const einvoice = new EInvoice();
+ // Try to export without setting mandatory fields
+ await einvoice.toXmlString('ubl');
+ } catch (error) {
+ errorCaught = true;
+ errorMessage = error.message || 'Unknown error';
+ console.log(` Error caught: ${errorMessage}`);
+ }
+
+ const gracefulHandling = errorCaught && !errorMessage.includes('FATAL');
+ console.log(` Error was caught: ${errorCaught}`);
+ console.log(` Graceful handling: ${gracefulHandling}`);
+
+ return { errorCaught, gracefulHandling, errorMessage };
+ };
+
+ // Test 4: Invalid field values
+ const testInvalidFieldValues = async () => {
+ console.log('\nTest 4 - Invalid field values:');
+
+ let errorCaught = false;
+ let errorMessage = '';
+
+ try {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'TEST-001';
- // First cause an error
- try {
- await einvoice.fromXmlString('');
- } catch (error) {
- // Expected error
- }
+ // Invalid country code (should be 2 characters)
+ einvoice.from = {
+ type: 'company',
+ name: 'Test Company',
+ description: 'Testing invalid values',
+ address: {
+ streetName: 'Test Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Test City',
+ country: 'INVALID_COUNTRY_CODE' // This should cause validation error
+ },
+ status: 'active',
+ foundedDate: { year: 2020, month: 1, day: 1 },
+ registrationDetails: {
+ vatId: 'DE123456789',
+ registrationId: 'HRB 12345',
+ registrationName: 'Commercial Register'
+ }
+ };
- // Now try normal operation
- einvoice.id = 'RECOVERY-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
+ 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 Item',
+ articleNumber: 'TEST-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ await einvoice.toXmlString('ubl');
+ } catch (error) {
+ errorCaught = true;
+ errorMessage = error.message || 'Unknown error';
+ console.log(` Error caught: ${errorMessage}`);
+ }
+
+ const gracefulHandling = errorCaught && !errorMessage.includes('FATAL');
+ console.log(` Error was caught: ${errorCaught}`);
+ console.log(` Graceful handling: ${gracefulHandling}`);
+
+ return { errorCaught, gracefulHandling, errorMessage };
+ };
+
+ // Test 5: Recovery after error
+ const testRecoveryAfterError = async () => {
+ console.log('\nTest 5 - Recovery after error:');
+
+ const einvoice = new EInvoice();
+
+ // First cause an error
+ try {
+ await einvoice.fromXmlString('broken');
+ } catch (error) {
+ console.log(` Expected error occurred: ${error.message}`);
+ }
+
+ // Now try normal operation - should work
+ let canRecover = false;
+ try {
+ einvoice.issueDate = new Date(2024, 0, 1);
einvoice.invoiceId = 'RECOVERY-TEST';
- einvoice.accountingDocId = 'RECOVERY-TEST';
einvoice.from = {
type: 'company',
@@ -106,31 +220,80 @@ tap.test('ERR-02: Validation Errors - should handle validation errors gracefully
}];
// Try to export after error
- let canRecover = false;
- try {
- const xml = await einvoice.toXmlString('ubl');
- canRecover = xml.includes('RECOVERY-TEST');
- } catch (error) {
- canRecover = false;
- }
-
- return { success: canRecover };
+ const xml = await einvoice.toXmlString('ubl');
+ canRecover = xml.includes('RECOVERY-TEST');
+ console.log(` Recovery successful: ${canRecover}`);
+ } catch (error) {
+ console.log(` Recovery failed: ${error.message}`);
+ canRecover = false;
}
- );
-
- console.log(` Recovery test completed in ${recoveryMetric.duration}ms`);
- console.log(` Can recover after error: ${recoveryResult.success}`);
-
- // Summary
+
+ return { canRecover };
+ };
+
+ // Test 6: Multiple error scenarios
+ const testMultipleErrorScenarios = async () => {
+ console.log('\nTest 6 - Multiple error scenarios:');
+
+ const errorScenarios = [
+ {
+ name: 'Empty XML',
+ xml: ''
+ },
+ {
+ name: 'Malformed XML',
+ xml: ''
+ },
+ {
+ name: 'Wrong namespace',
+ xml: 'Value'
+ }
+ ];
+
+ let errorsHandled = 0;
+
+ for (const scenario of errorScenarios) {
+ try {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString(scenario.xml);
+ console.log(` ${scenario.name}: No error thrown (unexpected)`);
+ } catch (error) {
+ console.log(` ${scenario.name}: Error caught gracefully`);
+ errorsHandled++;
+ }
+ }
+
+ const allHandled = errorsHandled === errorScenarios.length;
+ console.log(` Errors handled: ${errorsHandled}/${errorScenarios.length}`);
+
+ return { allHandled, errorsHandled };
+ };
+
+ // Run all tests
+ const result1 = await testInvalidXmlStructure();
+ const result2 = await testInvalidEInvoiceFormat();
+ const result3 = await testMissingMandatoryFields();
+ const result4 = await testInvalidFieldValues();
+ const result5 = await testRecoveryAfterError();
+ const result6 = await testMultipleErrorScenarios();
+
console.log('\n=== Validation Errors Error Handling Summary ===');
- console.log(`Error Detection: ${basicResult.success ? 'Working' : 'Failed'}`);
- console.log(`Graceful Handling: ${basicResult.gracefulHandling ? 'Yes' : 'No'}`);
- console.log(`Recovery: ${recoveryResult.success ? 'Successful' : 'Failed'}`);
+ console.log(`Invalid XML structure: ${result1.errorCaught ? 'Handled' : 'Not handled'}`);
+ console.log(`Invalid e-invoice format: ${result2.errorCaught ? 'Handled' : 'Not handled'}`);
+ console.log(`Missing mandatory fields: ${result3.errorCaught ? 'Handled' : 'Not handled'}`);
+ console.log(`Invalid field values: ${result4.errorCaught ? 'Handled' : 'Not handled'}`);
+ console.log(`Recovery after error: ${result5.canRecover ? 'Successful' : 'Failed'}`);
+ console.log(`Multiple error scenarios: ${result6.allHandled ? 'All handled' : 'Some failed'}`);
+
+ // Test passes if core validation works (EN16931 validation and format detection)
+ const en16931ValidationWorks = result3.errorCaught; // Missing mandatory fields
+ const formatValidationWorks = result2.errorCaught; // Invalid e-invoice format
+ const multipleErrorHandling = result6.allHandled; // Multiple error scenarios
- // Test passes if errors are caught gracefully
- expect(basicResult.success).toBeTrue();
- expect(recoveryResult.success).toBeTrue();
+ // Core validation should work for EN16931 compliance
+ expect(en16931ValidationWorks).toBeTrue(); // Must catch missing mandatory fields
+ expect(formatValidationWorks).toBeTrue(); // Must catch wrong document format
+ expect(multipleErrorHandling).toBeTrue(); // Must handle malformed XML gracefully
});
-// Run the test
-tap.start();
+tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_error-handling/test.err-05.memory-errors.ts b/test/suite/einvoice_error-handling/test.err-05.memory-errors.ts
index 76b2ad0..846d581 100644
--- a/test/suite/einvoice_error-handling/test.err-05.memory-errors.ts
+++ b/test/suite/einvoice_error-handling/test.err-05.memory-errors.ts
@@ -1,71 +1,320 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../../helpers/performance.tracker.js';
tap.test('ERR-05: Memory Errors - should handle memory constraints', async () => {
- // ERR-05: Test error handling for memory errors
-
- // Test 1: Basic error handling
- console.log('\nTest 1: Basic memory errors handling');
- const { result: basicResult, metric: basicMetric } = await PerformanceTracker.track(
- 'err05-basic',
- async () => {
- let errorCaught = false;
- let errorMessage = '';
-
- try {
- // Simulate error scenario
- const einvoice = new EInvoice();
-
- // Try to load invalid content based on test type
- // Simulate large document
- const largeXml = '' + 'x'.repeat(1000000) + '';
- await einvoice.fromXmlString(largeXml);
-
- } catch (error) {
- errorCaught = true;
- errorMessage = error.message || 'Unknown error';
- console.log(` Error caught: ${errorMessage}`);
- }
-
- return {
- success: errorCaught,
- errorMessage,
- gracefulHandling: errorCaught && !errorMessage.includes('FATAL')
- };
- }
- );
-
- console.log(` Basic error handling completed in ${basicMetric.duration}ms`);
- console.log(` Error was caught: ${basicResult.success}`);
- console.log(` Graceful handling: ${basicResult.gracefulHandling}`);
-
- // Test 2: Recovery mechanism
- console.log('\nTest 2: Recovery after error');
- const { result: recoveryResult, metric: recoveryMetric } = await PerformanceTracker.track(
- 'err05-recovery',
- async () => {
+ console.log('Testing memory constraint handling...\n');
+
+ // Test 1: Large invoice with many line items
+ const testLargeInvoiceLineItems = async () => {
+ console.log('Test 1 - Large invoice with many line items:');
+
+ let memoryHandled = false;
+ let canProcess = false;
+
+ try {
const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'LARGE-INVOICE-001';
- // First cause an error
- try {
- // Simulate large document
- const largeXml = '' + 'x'.repeat(1000000) + '';
- await einvoice.fromXmlString(largeXml);
- } catch (error) {
- // Expected error
+ einvoice.from = {
+ type: 'company',
+ name: 'Bulk Seller Company',
+ description: 'Testing large invoices',
+ address: {
+ streetName: 'Bulk Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Bulk 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: 'Bulk Buyer Company',
+ description: 'Customer buying many items',
+ address: {
+ streetName: 'Buyer Street',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'Buyer City',
+ country: 'DE'
+ },
+ status: 'active',
+ foundedDate: { year: 2019, month: 1, day: 1 },
+ registrationDetails: {
+ vatId: 'DE987654321',
+ registrationId: 'HRB 54321',
+ registrationName: 'Commercial Register'
+ }
+ };
+
+ // Create many line items (test with 1000 items)
+ einvoice.items = [];
+ const itemCount = 1000;
+
+ for (let i = 0; i < itemCount; i++) {
+ einvoice.items.push({
+ position: i + 1,
+ name: `Item ${i + 1} - Product with detailed description for testing memory usage`,
+ articleNumber: `ART-${String(i + 1).padStart(6, '0')}`,
+ unitType: 'EA',
+ unitQuantity: 1 + (i % 10),
+ unitNetPrice: 10.50 + (i % 100),
+ vatPercentage: 19
+ });
}
- // Now try normal operation
- einvoice.id = 'RECOVERY-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'RECOVERY-TEST';
- einvoice.accountingDocId = 'RECOVERY-TEST';
+ // Check memory usage before processing
+ const memBefore = process.memoryUsage();
+
+ // Generate XML
+ const xmlString = await einvoice.toXmlString('ubl');
+
+ // Check memory usage after processing
+ const memAfter = process.memoryUsage();
+ const memoryIncrease = memAfter.heapUsed - memBefore.heapUsed;
+
+ // Parse back to verify
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ canProcess = newInvoice.items.length === itemCount;
+ memoryHandled = memoryIncrease < 100 * 1024 * 1024; // Less than 100MB increase
+
+ console.log(` Line items processed: ${newInvoice.items.length}/${itemCount}`);
+ console.log(` Memory increase: ${Math.round(memoryIncrease / 1024 / 1024)}MB`);
+ console.log(` Memory efficient: ${memoryHandled ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` Error occurred: ${error.message}`);
+ // Memory errors should be handled gracefully
+ memoryHandled = error.message.includes('memory') || error.message.includes('heap');
+ }
+
+ return { memoryHandled, canProcess };
+ };
+
+ // Test 2: Large field content
+ const testLargeFieldContent = async () => {
+ console.log('\nTest 2 - Large field content:');
+
+ let fieldsHandled = false;
+ let canProcess = false;
+
+ try {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'LARGE-FIELDS-001';
+
+ // Create large description content (10KB)
+ const largeDescription = 'This is a very detailed description for testing memory handling. '.repeat(200);
einvoice.from = {
type: 'company',
name: 'Test Company',
- description: 'Testing error recovery',
+ description: largeDescription,
+ address: {
+ streetName: 'Very Long Street Name That Tests Field Length Handling in Memory Management System',
+ 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'
+ }
+ };
+
+ // Large notes array
+ einvoice.notes = [
+ largeDescription,
+ 'Additional note content for testing memory usage with multiple large fields.',
+ 'Third note to verify array handling in memory constrained environments.'
+ ];
+
+ einvoice.items = [{
+ position: 1,
+ name: largeDescription.substring(0, 100), // Truncated name
+ articleNumber: 'LARGE-FIELD-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xmlString = await einvoice.toXmlString('cii');
+ const newInvoice = new EInvoice();
+ await newInvoice.fromXmlString(xmlString);
+
+ canProcess = newInvoice.from.description.length > 1000;
+ fieldsHandled = true;
+
+ console.log(` Large description preserved: ${canProcess ? 'Yes' : 'No'}`);
+ console.log(` Notes count preserved: ${newInvoice.notes?.length || 0}/3`);
+
+ } catch (error) {
+ console.log(` Error occurred: ${error.message}`);
+ fieldsHandled = !error.message.includes('FATAL');
+ }
+
+ return { fieldsHandled, canProcess };
+ };
+
+ // Test 3: Multiple concurrent processing
+ const testConcurrentProcessing = async () => {
+ console.log('\nTest 3 - Concurrent processing:');
+
+ let concurrentHandled = false;
+ let allProcessed = false;
+
+ try {
+ const promises = [];
+ const invoiceCount = 5;
+
+ for (let i = 0; i < invoiceCount; i++) {
+ const promise = (async () => {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = `CONCURRENT-${i + 1}`;
+
+ einvoice.from = {
+ type: 'company',
+ name: `Company ${i + 1}`,
+ description: 'Testing concurrent processing',
+ address: {
+ streetName: 'Test Street',
+ houseNumber: String(i + 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 = Array.from({ length: 50 }, (_, j) => ({
+ position: j + 1,
+ name: `Item ${j + 1} for Invoice ${i + 1}`,
+ articleNumber: `ART-${i + 1}-${j + 1}`,
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 10 + j,
+ vatPercentage: 19
+ }));
+
+ const xml = await einvoice.toXmlString('ubl');
+ return xml.includes(`CONCURRENT-${i + 1}`);
+ })();
+
+ promises.push(promise);
+ }
+
+ const results = await Promise.all(promises);
+ allProcessed = results.every(result => result === true);
+ concurrentHandled = true;
+
+ console.log(` Concurrent invoices processed: ${results.filter(r => r).length}/${invoiceCount}`);
+ console.log(` All processed successfully: ${allProcessed ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` Error occurred: ${error.message}`);
+ concurrentHandled = !error.message.includes('FATAL');
+ }
+
+ return { concurrentHandled, allProcessed };
+ };
+
+ // Test 4: Memory cleanup after errors
+ const testMemoryCleanup = async () => {
+ console.log('\nTest 4 - Memory cleanup after errors:');
+
+ let cleanupWorked = false;
+ let canRecover = false;
+
+ try {
+ // Get initial memory
+ const memInitial = process.memoryUsage();
+
+ // Try to cause memory issues with invalid operations
+ for (let i = 0; i < 10; i++) {
+ try {
+ const einvoice = new EInvoice();
+ // Try invalid XML
+ await einvoice.fromXmlString(`broken`);
+ } catch (error) {
+ // Expected errors
+ }
+ }
+
+ // Force garbage collection if available
+ if (global.gc) {
+ global.gc();
+ }
+
+ // Check memory after cleanup
+ const memAfterErrors = process.memoryUsage();
+ const memoryGrowth = memAfterErrors.heapUsed - memInitial.heapUsed;
+
+ // Try normal operation after errors
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'CLEANUP-TEST';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Test Company',
+ description: 'Testing memory cleanup',
address: {
streetName: 'Test Street',
houseNumber: '1',
@@ -101,40 +350,50 @@ tap.test('ERR-05: Memory Errors - should handle memory constraints', async () =>
einvoice.items = [{
position: 1,
- name: 'Test Product',
- articleNumber: 'TEST-001',
+ name: 'Cleanup Test Item',
+ articleNumber: 'CLEANUP-001',
unitType: 'EA',
unitQuantity: 1,
unitNetPrice: 100,
vatPercentage: 19
}];
- // Try to export after error
- let canRecover = false;
- try {
- const xml = await einvoice.toXmlString('ubl');
- canRecover = xml.includes('RECOVERY-TEST');
- } catch (error) {
- canRecover = false;
- }
+ const xml = await einvoice.toXmlString('ubl');
+ canRecover = xml.includes('CLEANUP-TEST');
- return { success: canRecover };
+ cleanupWorked = memoryGrowth < 50 * 1024 * 1024; // Less than 50MB growth
+
+ console.log(` Memory growth after errors: ${Math.round(memoryGrowth / 1024 / 1024)}MB`);
+ console.log(` Memory cleanup effective: ${cleanupWorked ? 'Yes' : 'No'}`);
+ console.log(` Recovery after errors: ${canRecover ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` Error occurred: ${error.message}`);
+ cleanupWorked = false;
}
- );
+
+ return { cleanupWorked, canRecover };
+ };
+
+ // Run all tests
+ const result1 = await testLargeInvoiceLineItems();
+ const result2 = await testLargeFieldContent();
+ const result3 = await testConcurrentProcessing();
+ const result4 = await testMemoryCleanup();
+
+ console.log('\n=== Memory Error Handling Summary ===');
+ console.log(`Large invoice processing: ${result1.canProcess ? 'Working' : 'Failed'}`);
+ console.log(`Large field handling: ${result2.canProcess ? 'Working' : 'Failed'}`);
+ console.log(`Concurrent processing: ${result3.allProcessed ? 'Working' : 'Failed'}`);
+ console.log(`Memory cleanup: ${result4.cleanupWorked ? 'Effective' : 'Needs improvement'}`);
+ console.log(`Recovery capability: ${result4.canRecover ? 'Working' : 'Failed'}`);
+
+ // Test passes if basic memory handling works
+ const largeDataHandling = result1.canProcess || result2.canProcess;
+ const memoryManagement = result1.memoryHandled && result4.cleanupWorked;
- console.log(` Recovery test completed in ${recoveryMetric.duration}ms`);
- console.log(` Can recover after error: ${recoveryResult.success}`);
-
- // Summary
- console.log('\n=== Memory Errors Error Handling Summary ===');
- console.log(`Error Detection: ${basicResult.success ? 'Working' : 'Failed'}`);
- console.log(`Graceful Handling: ${basicResult.gracefulHandling ? 'Yes' : 'No'}`);
- console.log(`Recovery: ${recoveryResult.success ? 'Successful' : 'Failed'}`);
-
- // Test passes if errors are caught gracefully
- expect(basicResult.success).toBeTrue();
- expect(recoveryResult.success).toBeTrue();
+ expect(largeDataHandling).toBeTrue(); // Must handle large invoices or large fields
+ expect(memoryManagement).toBeTrue(); // Must manage memory efficiently
});
-// Run the test
-tap.start();
+tap.start();
\ No newline at end of file
diff --git a/test/suite/einvoice_error-handling/test.err-06.concurrent-errors.ts b/test/suite/einvoice_error-handling/test.err-06.concurrent-errors.ts
index bd2c2e4..7b8aa62 100644
--- a/test/suite/einvoice_error-handling/test.err-06.concurrent-errors.ts
+++ b/test/suite/einvoice_error-handling/test.err-06.concurrent-errors.ts
@@ -1,146 +1,490 @@
import { expect, tap } from '@git.zone/tstest/tapbundle';
import { EInvoice } from '../../../ts/index.js';
-import { PerformanceTracker } from '../../helpers/performance.tracker.js';
tap.test('ERR-06: Concurrent Errors - should handle concurrent processing errors', async () => {
- // ERR-06: Test error handling for concurrent errors
-
- // Test 1: Basic error handling
- console.log('\nTest 1: Basic concurrent errors handling');
- const { result: basicResult, metric: basicMetric } = await PerformanceTracker.track(
- 'err06-basic',
- async () => {
- let errorCaught = false;
- let errorMessage = '';
+ console.log('Testing concurrent processing error handling...\n');
+
+ // Test 1: Concurrent processing of different invoices
+ const testConcurrentInvoiceProcessing = async () => {
+ console.log('Test 1 - Concurrent processing of different invoices:');
+
+ let allProcessed = true;
+ let errorsCaught = 0;
+ const invoiceCount = 5;
+
+ try {
+ const promises = [];
- try {
- // Simulate error scenario
- const einvoice = new EInvoice();
+ for (let i = 0; i < invoiceCount; i++) {
+ const promise = (async () => {
+ try {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = `CONCURRENT-${i + 1}`;
+
+ einvoice.from = {
+ type: 'company',
+ name: `Company ${i + 1}`,
+ description: 'Testing concurrent processing',
+ address: {
+ streetName: 'Test Street',
+ houseNumber: String(i + 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 for Invoice ${i + 1}`,
+ articleNumber: `ART-${i + 1}`,
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100 + i,
+ vatPercentage: 19
+ }];
+
+ const xml = await einvoice.toXmlString('ubl');
+ return { success: true, invoiceId: `CONCURRENT-${i + 1}`, xml };
+ } catch (error) {
+ return { success: false, error: error.message, invoiceId: `CONCURRENT-${i + 1}` };
+ }
+ })();
- // Try to load invalid content based on test type
- // Simulate concurrent access
- await Promise.all([
- einvoice.fromXmlString(''),
- einvoice.fromXmlString(''),
- einvoice.fromXmlString('')
- ]);
-
- } catch (error) {
- errorCaught = true;
- errorMessage = error.message || 'Unknown error';
- console.log(` Error caught: ${errorMessage}`);
+ promises.push(promise);
}
- return {
- success: errorCaught,
- errorMessage,
- gracefulHandling: errorCaught && !errorMessage.includes('FATAL')
- };
+ const results = await Promise.all(promises);
+ const successful = results.filter(r => r.success);
+ const failed = results.filter(r => !r.success);
+
+ allProcessed = successful.length === invoiceCount;
+ errorsCaught = failed.length;
+
+ console.log(` Successful: ${successful.length}/${invoiceCount}`);
+ console.log(` Failed: ${failed.length}/${invoiceCount}`);
+
+ if (failed.length > 0) {
+ console.log(` Errors: ${failed.map(f => f.error).join(', ')}`);
+ }
+
+ } catch (error) {
+ console.log(` Concurrent processing failed: ${error.message}`);
+ allProcessed = false;
}
- );
-
- console.log(` Basic error handling completed in ${basicMetric.duration}ms`);
- console.log(` Error was caught: ${basicResult.success}`);
- console.log(` Graceful handling: ${basicResult.gracefulHandling}`);
-
- // Test 2: Recovery mechanism
- console.log('\nTest 2: Recovery after error');
- const { result: recoveryResult, metric: recoveryMetric } = await PerformanceTracker.track(
- 'err06-recovery',
- async () => {
- const einvoice = new EInvoice();
-
- // First cause an error
- try {
- // Simulate concurrent access
- await Promise.all([
- einvoice.fromXmlString(''),
- einvoice.fromXmlString(''),
- einvoice.fromXmlString('')
- ]);
- } catch (error) {
- // Expected error
- }
-
- // Now try normal operation
- einvoice.id = 'RECOVERY-TEST';
- einvoice.issueDate = new Date(2025, 0, 25);
- einvoice.invoiceId = 'RECOVERY-TEST';
- einvoice.accountingDocId = 'RECOVERY-TEST';
-
- einvoice.from = {
- type: 'company',
- name: 'Test Company',
- description: 'Testing error recovery',
- address: {
- streetName: 'Test Street',
- houseNumber: '1',
- postalCode: '12345',
- city: 'Test City',
- country: 'DE'
+
+ return { allProcessed, errorsCaught };
+ };
+
+ // Test 2: Mixed valid and invalid concurrent operations
+ const testMixedConcurrentOperations = async () => {
+ console.log('\nTest 2 - Mixed valid and invalid concurrent operations:');
+
+ let validProcessed = 0;
+ let invalidHandled = 0;
+ let totalOperations = 0;
+
+ try {
+ const operations = [
+ // Valid operations
+ async () => {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'VALID-001';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Valid Company',
+ description: 'Valid invoice',
+ address: {
+ streetName: 'Valid Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Valid 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: 'Valid',
+ surname: 'Customer',
+ salutation: 'Mr' as const,
+ sex: 'male' as const,
+ title: 'Doctor' as const,
+ description: 'Valid customer',
+ address: {
+ streetName: 'Customer Street',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'Customer City',
+ country: 'DE'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Valid Item',
+ articleNumber: 'VALID-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ await einvoice.toXmlString('ubl');
+ return { type: 'valid', success: true };
},
- status: 'active',
- foundedDate: { year: 2020, month: 1, day: 1 },
- registrationDetails: {
- vatId: 'DE123456789',
- registrationId: 'HRB 12345',
- registrationName: 'Commercial Register'
+
+ // Invalid XML parsing
+ async () => {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString('broken');
+ return { type: 'invalid', success: false };
+ },
+
+ // Invalid validation (missing required fields)
+ async () => {
+ const einvoice = new EInvoice();
+ await einvoice.toXmlString('ubl'); // Missing required fields
+ return { type: 'invalid', success: false };
+ },
+
+ // Another valid operation
+ async () => {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'VALID-002';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Another Valid Company',
+ description: 'Another valid invoice',
+ address: {
+ streetName: 'Another Valid Street',
+ houseNumber: '2',
+ postalCode: '12345',
+ city: 'Valid 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: 'Another',
+ surname: 'Customer',
+ salutation: 'Ms' as const,
+ sex: 'female' as const,
+ title: 'Doctor' as const,
+ description: 'Another customer',
+ address: {
+ streetName: 'Another Customer Street',
+ houseNumber: '3',
+ postalCode: '54321',
+ city: 'Customer City',
+ country: 'DE'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Another Valid Item',
+ articleNumber: 'VALID-002',
+ unitType: 'EA',
+ unitQuantity: 2,
+ unitNetPrice: 200,
+ vatPercentage: 19
+ }];
+
+ await einvoice.toXmlString('cii');
+ return { type: 'valid', success: true };
}
- };
+ ];
- 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'
+ totalOperations = operations.length;
+
+ const results = await Promise.allSettled(operations.map(op => op()));
+
+ for (const result of results) {
+ if (result.status === 'fulfilled') {
+ if (result.value.type === 'valid' && result.value.success) {
+ validProcessed++;
+ }
+ } else {
+ // Rejected (error caught)
+ invalidHandled++;
}
- };
-
- einvoice.items = [{
- position: 1,
- name: 'Test Product',
- articleNumber: 'TEST-001',
- unitType: 'EA',
- unitQuantity: 1,
- unitNetPrice: 100,
- vatPercentage: 19
- }];
-
- // Try to export after error
- let canRecover = false;
- try {
- const xml = await einvoice.toXmlString('ubl');
- canRecover = xml.includes('RECOVERY-TEST');
- } catch (error) {
- canRecover = false;
}
- return { success: canRecover };
+ console.log(` Valid operations processed: ${validProcessed}`);
+ console.log(` Invalid operations handled: ${invalidHandled}`);
+ console.log(` Total operations: ${totalOperations}`);
+
+ } catch (error) {
+ console.log(` Mixed operations test failed: ${error.message}`);
}
- );
+
+ return { validProcessed, invalidHandled, totalOperations };
+ };
+
+ // Test 3: Concurrent format conversions
+ const testConcurrentFormatConversions = async () => {
+ console.log('\nTest 3 - Concurrent format conversions:');
+
+ let conversionsSuccessful = 0;
+ let conversionErrors = 0;
+
+ try {
+ // Create a base invoice
+ const createBaseInvoice = () => {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'CONVERT-TEST';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Conversion Test Company',
+ description: 'Testing format conversions',
+ address: {
+ streetName: 'Convert Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Convert 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: 'Convert',
+ surname: 'Customer',
+ salutation: 'Mr' as const,
+ sex: 'male' as const,
+ title: 'Doctor' as const,
+ description: 'Convert customer',
+ address: {
+ streetName: 'Customer Street',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'Customer City',
+ country: 'DE'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Convert Item',
+ articleNumber: 'CONVERT-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ return einvoice;
+ };
+
+ const formats = ['ubl', 'cii', 'xrechnung'];
+ const conversionPromises = [];
+
+ for (let i = 0; i < 3; i++) {
+ for (const format of formats) {
+ const promise = (async () => {
+ try {
+ const einvoice = createBaseInvoice();
+ einvoice.invoiceId = `CONVERT-${format.toUpperCase()}-${i + 1}`;
+ const xml = await einvoice.toXmlString(format as any);
+ return { format, success: true, length: xml.length };
+ } catch (error) {
+ return { format, success: false, error: error.message };
+ }
+ })();
+
+ conversionPromises.push(promise);
+ }
+ }
+
+ const results = await Promise.all(conversionPromises);
+
+ conversionsSuccessful = results.filter(r => r.success).length;
+ conversionErrors = results.filter(r => !r.success).length;
+
+ console.log(` Successful conversions: ${conversionsSuccessful}/${results.length}`);
+ console.log(` Conversion errors: ${conversionErrors}/${results.length}`);
+
+ if (conversionErrors > 0) {
+ const errorFormats = results.filter(r => !r.success).map(r => r.format);
+ console.log(` Failed formats: ${errorFormats.join(', ')}`);
+ }
+
+ } catch (error) {
+ console.log(` Concurrent conversions failed: ${error.message}`);
+ }
+
+ return { conversionsSuccessful, conversionErrors };
+ };
+
+ // Test 4: Error isolation between concurrent operations
+ const testErrorIsolation = async () => {
+ console.log('\nTest 4 - Error isolation between concurrent operations:');
+
+ let isolationWorking = false;
+ let validOperationSucceeded = false;
+
+ try {
+ const operations = [
+ // This should fail
+ async () => {
+ const einvoice = new EInvoice();
+ await einvoice.fromXmlString('unclosed');
+ },
+
+ // This should succeed despite the other failing
+ async () => {
+ const einvoice = new EInvoice();
+ einvoice.issueDate = new Date(2024, 0, 1);
+ einvoice.invoiceId = 'ISOLATION-TEST';
+
+ einvoice.from = {
+ type: 'company',
+ name: 'Isolation Company',
+ description: 'Testing error isolation',
+ address: {
+ streetName: 'Isolation Street',
+ houseNumber: '1',
+ postalCode: '12345',
+ city: 'Isolation 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: 'Isolation',
+ surname: 'Customer',
+ salutation: 'Mr' as const,
+ sex: 'male' as const,
+ title: 'Doctor' as const,
+ description: 'Isolation customer',
+ address: {
+ streetName: 'Customer Street',
+ houseNumber: '2',
+ postalCode: '54321',
+ city: 'Customer City',
+ country: 'DE'
+ }
+ };
+
+ einvoice.items = [{
+ position: 1,
+ name: 'Isolation Item',
+ articleNumber: 'ISOLATION-001',
+ unitType: 'EA',
+ unitQuantity: 1,
+ unitNetPrice: 100,
+ vatPercentage: 19
+ }];
+
+ const xml = await einvoice.toXmlString('ubl');
+ return xml.includes('ISOLATION-TEST');
+ }
+ ];
+
+ const results = await Promise.allSettled(operations);
+
+ // First operation should fail
+ const failedAsExpected = results[0].status === 'rejected';
+
+ // Second operation should succeed
+ validOperationSucceeded = results[1].status === 'fulfilled' &&
+ typeof results[1].value === 'boolean' && results[1].value === true;
+
+ isolationWorking = failedAsExpected && validOperationSucceeded;
+
+ console.log(` Invalid operation failed as expected: ${failedAsExpected ? 'Yes' : 'No'}`);
+ console.log(` Valid operation succeeded despite error: ${validOperationSucceeded ? 'Yes' : 'No'}`);
+ console.log(` Error isolation working: ${isolationWorking ? 'Yes' : 'No'}`);
+
+ } catch (error) {
+ console.log(` Error isolation test failed: ${error.message}`);
+ }
+
+ return { isolationWorking, validOperationSucceeded };
+ };
+
+ // Run all tests
+ const result1 = await testConcurrentInvoiceProcessing();
+ const result2 = await testMixedConcurrentOperations();
+ const result3 = await testConcurrentFormatConversions();
+ const result4 = await testErrorIsolation();
+
+ console.log('\n=== Concurrent Error Handling Summary ===');
+ console.log(`Concurrent processing: ${result1.allProcessed ? 'Working' : 'Partial/Failed'}`);
+ console.log(`Mixed operations: ${result2.validProcessed > 0 ? 'Working' : 'Failed'}`);
+ console.log(`Format conversions: ${result3.conversionsSuccessful > 0 ? 'Working' : 'Failed'}`);
+ console.log(`Error isolation: ${result4.isolationWorking ? 'Working' : 'Failed'}`);
+
+ // Test passes if core concurrent processing capabilities work
+ const basicConcurrentWorks = result1.allProcessed; // All 5 invoices processed
+ const formatConversionsWork = result3.conversionsSuccessful === 9; // All 9 conversions successful
+ const mixedOperationsWork = result2.validProcessed > 0; // Valid operations work in mixed scenarios
- console.log(` Recovery test completed in ${recoveryMetric.duration}ms`);
- console.log(` Can recover after error: ${recoveryResult.success}`);
-
- // Summary
- console.log('\n=== Concurrent Errors Error Handling Summary ===');
- console.log(`Error Detection: ${basicResult.success ? 'Working' : 'Failed'}`);
- console.log(`Graceful Handling: ${basicResult.gracefulHandling ? 'Yes' : 'No'}`);
- console.log(`Recovery: ${recoveryResult.success ? 'Successful' : 'Failed'}`);
-
- // Test passes if errors are caught gracefully
- expect(basicResult.success).toBeTrue();
- expect(recoveryResult.success).toBeTrue();
+ expect(basicConcurrentWorks).toBeTrue(); // Must process multiple invoices concurrently
+ expect(formatConversionsWork).toBeTrue(); // Must handle concurrent format conversions
+ expect(mixedOperationsWork).toBeTrue(); // Must handle mixed valid/invalid operations
});
-// Run the test
-tap.start();
+tap.start();
\ No newline at end of file