437 lines
15 KiB
TypeScript
437 lines
15 KiB
TypeScript
import { expect, tap } from '@git.zone/tstest/tapbundle';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
|
|
tap.test('EDGE-06: Circular References - should handle circular reference scenarios', async () => {
|
|
console.log('Testing circular reference handling in e-invoices...\n');
|
|
|
|
// Test 1: Self-referencing invoice documents
|
|
const testSelfReferencingInvoice = async () => {
|
|
try {
|
|
const einvoice = new EInvoice();
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
einvoice.id = 'CIRC-001';
|
|
|
|
// 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: 'Test Product',
|
|
articleNumber: 'CIRC-001',
|
|
unitType: 'EA',
|
|
unitQuantity: 1,
|
|
unitNetPrice: 100,
|
|
vatPercentage: 19
|
|
}];
|
|
|
|
// Try to create XML - should not cause infinite loops
|
|
const xmlString = await einvoice.toXmlString('ubl');
|
|
|
|
// 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('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 = `<?xml version="1.0" encoding="UTF-8"?>
|
|
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
|
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
|
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
|
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
|
<cbc:ID>CIRCULAR-REF-TEST</cbc:ID>
|
|
<cbc:IssueDate>2024-01-01</cbc:IssueDate>
|
|
<cbc:DocumentCurrencyCode>EUR</cbc:DocumentCurrencyCode>
|
|
|
|
<!-- Potentially problematic: referring to same invoice -->
|
|
<cac:InvoiceDocumentReference>
|
|
<cbc:ID>CIRCULAR-REF-TEST</cbc:ID>
|
|
<cbc:DocumentTypeCode>380</cbc:DocumentTypeCode>
|
|
</cac:InvoiceDocumentReference>
|
|
|
|
<cac:AccountingSupplierParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>Circular Test Company</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Test Street</cbc:StreetName>
|
|
<cbc:BuildingNumber>1</cbc:BuildingNumber>
|
|
<cbc:PostalZone>12345</cbc:PostalZone>
|
|
<cbc:CityName>Test City</cbc:CityName>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingSupplierParty>
|
|
|
|
<cac:AccountingCustomerParty>
|
|
<cac:Party>
|
|
<cac:PartyName>
|
|
<cbc:Name>Customer Company</cbc:Name>
|
|
</cac:PartyName>
|
|
<cac:PostalAddress>
|
|
<cbc:StreetName>Customer Street</cbc:StreetName>
|
|
<cbc:BuildingNumber>2</cbc:BuildingNumber>
|
|
<cbc:PostalZone>54321</cbc:PostalZone>
|
|
<cbc:CityName>Customer City</cbc:CityName>
|
|
<cac:Country>
|
|
<cbc:IdentificationCode>DE</cbc:IdentificationCode>
|
|
</cac:Country>
|
|
</cac:PostalAddress>
|
|
</cac:Party>
|
|
</cac:AccountingCustomerParty>
|
|
|
|
<cac:InvoiceLine>
|
|
<cbc:ID>1</cbc:ID>
|
|
<cbc:InvoicedQuantity unitCode="EA">1</cbc:InvoicedQuantity>
|
|
<cbc:LineExtensionAmount currencyID="EUR">100.00</cbc:LineExtensionAmount>
|
|
<cac:Item>
|
|
<cbc:Name>Test Product</cbc:Name>
|
|
</cac:Item>
|
|
<cac:Price>
|
|
<cbc:PriceAmount currencyID="EUR">100.00</cbc:PriceAmount>
|
|
</cac:Price>
|
|
</cac:InvoiceLine>
|
|
</Invoice>`;
|
|
|
|
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
|
|
tap.start(); |