From 16e2bd6b1a2c34cc985326ee0267839896a9c9f8 Mon Sep 17 00:00:00 2001 From: Philipp Kunz Date: Wed, 28 May 2025 14:46:32 +0000 Subject: [PATCH] fix(compliance): improve compliance --- package.json | 4 +- pnpm-lock.yaml | 80 +- .../test.edge-04.unusual-charsets.ts | 830 +++++------ .../test.edge-05.zero-byte-pdf.ts | 508 +++---- .../test.edge-06.circular-references.ts | 1066 ++++++--------- .../test.edge-07.max-field-lengths.ts | 1214 ++++++++--------- .../test.enc-04.character-escaping.ts | 475 +++++-- .../test.enc-05.special-characters.ts | 505 +++++-- .../test.enc-06.namespace-declarations.ts | 515 +++++-- .../test.enc-07.attribute-encoding.ts | 354 +++-- .../test.enc-08.mixed-content.ts | 351 +++-- .../test.enc-09.encoding-errors.ts | 291 +++- .../test.enc-10.cross-format-encoding.ts | 331 ++++- .../test.err-02.validation-errors.ts | 311 ++++- .../test.err-05.memory-errors.ts | 423 ++++-- .../test.err-06.concurrent-errors.ts | 598 ++++++-- 16 files changed, 4718 insertions(+), 3138 deletions(-) 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(/>([^<]*)')) { + 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(`&#x${char.charCodeAt(0).toString(16)};`) + ).length; + + const currencyPreserved = specialChars.currency.split('').filter(char => + xmlString.includes(char) || + xmlString.includes(`&#${char.charCodeAt(0)};`) || + xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`) + ).length; + + const symbolsPreserved = specialChars.symbols.split('').filter(char => + xmlString.includes(char) || + xmlString.includes(`&#${char.charCodeAt(0)};`) || + xmlString.includes(`&#x${char.charCodeAt(0).toString(16)};`) + ).length; + + return { + mathPreserved, + currencyPreserved, + symbolsPreserved, + totalMath: specialChars.mathematical.length, + totalCurrency: specialChars.currency.length, + totalSymbols: specialChars.symbols.length, + xmlString + }; + }; + + const unicodeResult = await testUnicodeSpecialChars(); + console.log('Test 1 - Unicode special characters:'); + console.log(` Mathematical symbols: ${unicodeResult.mathPreserved}/${unicodeResult.totalMath} preserved`); + console.log(` Currency symbols: ${unicodeResult.currencyPreserved}/${unicodeResult.totalCurrency} preserved`); + console.log(` Other symbols: ${unicodeResult.symbolsPreserved}/${unicodeResult.totalSymbols} preserved`); + + // Test 2: Control characters and whitespace + const testControlCharacters = async () => { + const einvoice = new EInvoice(); + einvoice.id = 'CONTROL-CHARS-TEST'; + einvoice.date = Date.now(); + einvoice.currency = 'EUR'; + + // Test various whitespace and control characters + einvoice.subject = 'Control chars test:\ttab\nnewline\rcarriage return'; + einvoice.notes = [ + 'Tab separated:\tvalue1\tvalue2\tvalue3', + 'Line break:\nSecond line\nThird line', + 'Mixed whitespace: spaces \t tabs \r\n mixed' + ]; + + einvoice.from = { + type: 'company', + name: 'Control\tCharacters\nCompany', + description: 'Company\twith\ncontrol\rcharacters', + address: { + streetName: 'Control Street', + houseNumber: '1', + postalCode: '12345', + city: 'Test City', + country: 'DE' + }, + status: 'active', + foundedDate: { year: 2020, month: 1, day: 1 }, + registrationDetails: { + vatId: 'DE123456789', + registrationId: 'HRB 12345', + registrationName: 'Registry' + } + }; + + einvoice.to = { + type: 'company', + name: 'Customer', + description: 'Normal customer', + address: { + streetName: 'Customer Street', + houseNumber: '2', + postalCode: '54321', + city: 'Customer City', + country: 'DE' + }, + status: 'active', + foundedDate: { year: 2019, month: 1, day: 1 }, + registrationDetails: { + vatId: 'DE987654321', + registrationId: 'HRB 54321', + registrationName: 'Customer Registry' + } + }; + + einvoice.items = [{ + position: 1, + name: 'Product\twith\ncontrol\rchars', + unitType: 'C62', + unitQuantity: 1, + unitNetPrice: 100, + vatPercentage: 19 + }]; + + const xmlString = await einvoice.toXmlString('ubl'); + + // Check how control characters are handled + const hasTabHandling = xmlString.includes(' ') || xmlString.includes(' ') || + xmlString.includes('\t') || !xmlString.includes('Control\tCharacters'); + const hasNewlineHandling = xmlString.includes(' ') || xmlString.includes(' ') || + xmlString.includes('\n') || !xmlString.includes('Characters\nCompany'); + const hasCarriageReturnHandling = xmlString.includes(' ') || xmlString.includes(' ') || + xmlString.includes('\r') || !xmlString.includes('control\rcharacters'); + + return { + hasTabHandling, + hasNewlineHandling, + hasCarriageReturnHandling, + xmlString + }; + }; + + const controlResult = await testControlCharacters(); + console.log('\nTest 2 - Control characters and whitespace:'); + console.log(` Tab handling: ${controlResult.hasTabHandling ? 'Yes' : 'No'}`); + console.log(` Newline handling: ${controlResult.hasNewlineHandling ? 'Yes' : 'No'}`); + console.log(` Carriage return handling: ${controlResult.hasCarriageReturnHandling ? 'Yes' : 'No'}`); + + // Test 3: Emojis and extended Unicode + const testEmojisAndExtended = async () => { + const einvoice = new EInvoice(); + einvoice.id = 'EMOJI-TEST'; + einvoice.date = Date.now(); + einvoice.currency = 'EUR'; + + // Test emojis and extended Unicode + const emojis = '😀😃😄😁😆😅🤣😂🙂🙃😉😊😇🥰😍🤩😘😗☺😚😙🥲😋😛😜🤪😝🤑🤗🤭🤫🤔🤐🤨😐😑😶😏😒🙄😬🤥😌😔😪🤤😴😷🤒🤕🤢🤮🤧🥵🥶🥴😵🤯🤠🥳🥸😎🤓🧐😕😟🙁☹😮😯😲😳🥺😦😧😨😰😥😢😭😱😖😣😞😓😩😫🥱😤😡😠🤬😈👿💀☠💩🤡👹👺👻👽👾🤖😺😸😹😻😼😽🙀😿😾🙈🙉🙊💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤'; + + einvoice.subject = `Emoji test: ${emojis.substring(0, 20)}`; + einvoice.notes = [ + `Faces: ${emojis.substring(0, 50)}`, + `Hearts: 💋💌💘💝💖💗💓💞💕💟❣💔❤🧡💛💚💙💜🤎🖤🤍`, + `Objects: 💯💢💥💫💦💨🕳💣💬👁🗨🗯💭💤` + ]; + + einvoice.from = { + type: 'company', + name: 'Emoji Company 😊', + description: 'Company with emojis 🏢', + address: { + streetName: 'Happy Street 😃', + houseNumber: '1️⃣', + postalCode: '12345', + city: 'Emoji City 🌆', + country: 'DE' + }, + status: 'active', + foundedDate: { year: 2020, month: 1, day: 1 }, + registrationDetails: { + vatId: 'DE123456789', + registrationId: 'HRB 12345', + registrationName: 'Registry 📝' + } + }; + + einvoice.to = { + type: 'company', + name: 'Customer 🛍️', + description: 'Shopping customer', + address: { + streetName: 'Customer Street', + houseNumber: '2', + postalCode: '54321', + city: 'Customer City', + country: 'DE' + }, + status: 'active', + foundedDate: { year: 2019, month: 1, day: 1 }, + registrationDetails: { + vatId: 'DE987654321', + registrationId: 'HRB 54321', + registrationName: 'Customer Registry' + } + }; + + einvoice.items = [{ + position: 1, + name: 'Emoji Product 📦', + unitType: 'C62', + unitQuantity: 1, + unitNetPrice: 100, + vatPercentage: 19 + }]; + + const xmlString = await einvoice.toXmlString('ubl'); + + // Check if emojis are preserved or encoded + const emojiCount = emojis.split('').filter(char => { + const codePoint = char.codePointAt(0); + return codePoint && codePoint > 0xFFFF; // Emojis are typically above the BMP + }).length; + + const preservedEmojis = emojis.split('').filter(char => { + const codePoint = char.codePointAt(0); + if (!codePoint || codePoint <= 0xFFFF) return false; + return xmlString.includes(char) || + xmlString.includes(`&#${codePoint};`) || + xmlString.includes(`&#x${codePoint.toString(16)};`); + }).length; + + return { + emojiCount, + preservedEmojis, + preservationRate: emojiCount > 0 ? (preservedEmojis / emojiCount) * 100 : 0 + }; + }; + + const emojiResult = await testEmojisAndExtended(); + console.log('\nTest 3 - Emojis and extended Unicode:'); + console.log(` Emoji preservation: ${emojiResult.preservedEmojis}/${emojiResult.emojiCount} (${emojiResult.preservationRate.toFixed(1)}%)`); + + // Test 4: XML predefined entities in content + const testXmlPredefinedEntities = async () => { + const xmlWithEntities = ` + + 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