fix(compliance): improve compliance
This commit is contained in:
@ -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
|
||||
|
@ -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\n<xml><invoice>test</invoice></xml>\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 <invoice></invoice>
|
||||
'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();
|
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user