einvoice/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts

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();