einvoice/test/suite/einvoice_edge-cases/test.edge-06.circular-references.ts
2025-05-26 04:04:51 +00:00

540 lines
16 KiB
TypeScript

import { tap } from '@git.zone/tstest/tapbundle';
import * as plugins from '../plugins.js';
import { EInvoice } from '../../../ts/index.js';
import { PerformanceTracker } from '../performance.tracker.js';
const performanceTracker = new PerformanceTracker('EDGE-06: Circular References');
tap.test('EDGE-06: Circular References - should handle circular reference scenarios', async (t) => {
const einvoice = new EInvoice();
// Test 1: ID reference cycles in XML
const idReferenceCycles = await performanceTracker.measureAsync(
'id-reference-cycles',
async () => {
const circularXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>INV-001</ID>
<RelatedInvoice idref="INV-002"/>
<Items>
<Item id="item1">
<RelatedItem idref="item2"/>
<Price>100</Price>
</Item>
<Item id="item2">
<RelatedItem idref="item3"/>
<Price>200</Price>
</Item>
<Item id="item3">
<RelatedItem idref="item1"/>
<Price>300</Price>
</Item>
</Items>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(circularXML);
// Try to resolve references
const resolved = await einvoice.resolveReferences(parsed, {
maxDepth: 10,
detectCycles: true
});
return {
parsed: true,
hasCircularRefs: resolved?.hasCircularReferences || false,
cyclesDetected: resolved?.detectedCycles || [],
resolutionStopped: resolved?.stoppedAtDepth || false
};
} catch (error) {
return {
parsed: false,
error: error.message,
cycleError: error.message.includes('circular') || error.message.includes('cycle')
};
}
}
);
t.ok(idReferenceCycles.parsed || idReferenceCycles.cycleError,
'Circular ID references were handled');
// Test 2: Entity reference loops
const entityReferenceLoops = await performanceTracker.measureAsync(
'entity-reference-loops',
async () => {
const loopingEntities = [
{
name: 'direct-loop',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Invoice [
<!ENTITY a "&b;">
<!ENTITY b "&a;">
]>
<Invoice>
<Note>&a;</Note>
</Invoice>`
},
{
name: 'indirect-loop',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Invoice [
<!ENTITY a "&b;">
<!ENTITY b "&c;">
<!ENTITY c "&a;">
]>
<Invoice>
<Note>&a;</Note>
</Invoice>`
},
{
name: 'self-reference',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE Invoice [
<!ENTITY recursive "&recursive;">
]>
<Invoice>
<Note>&recursive;</Note>
</Invoice>`
}
];
const results = [];
for (const test of loopingEntities) {
try {
await einvoice.parseXML(test.xml);
results.push({
type: test.name,
handled: true,
method: 'parsed-without-expansion'
});
} catch (error) {
results.push({
type: test.name,
handled: true,
method: 'rejected',
error: error.message
});
}
}
return results;
}
);
entityReferenceLoops.forEach(result => {
t.ok(result.handled, `Entity loop ${result.type} was handled`);
});
// Test 3: Schema import cycles
const schemaImportCycles = await performanceTracker.measureAsync(
'schema-import-cycles',
async () => {
// Simulate schemas that import each other
const schemas = {
'schema1.xsd': `<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="schema2.xsd"/>
<xs:element name="Invoice" type="InvoiceType"/>
</xs:schema>`,
'schema2.xsd': `<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="schema3.xsd"/>
<xs:complexType name="InvoiceType"/>
</xs:schema>`,
'schema3.xsd': `<?xml version="1.0"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:import schemaLocation="schema1.xsd"/>
</xs:schema>`
};
try {
const validation = await einvoice.validateWithSchemas(schemas, {
maxImportDepth: 10,
detectImportCycles: true
});
return {
handled: true,
cycleDetected: validation?.importCycleDetected || false,
importChain: validation?.importChain || []
};
} catch (error) {
return {
handled: true,
error: error.message,
isCycleError: error.message.includes('import') && error.message.includes('cycle')
};
}
}
);
t.ok(schemaImportCycles.handled, 'Schema import cycles were handled');
// Test 4: Object graph cycles in parsed data
const objectGraphCycles = await performanceTracker.measureAsync(
'object-graph-cycles',
async () => {
// Create invoice with potential object cycles
const invoice = {
id: 'INV-001',
items: [],
parent: null
};
const item1 = {
id: 'ITEM-001',
invoice: invoice,
relatedItems: []
};
const item2 = {
id: 'ITEM-002',
invoice: invoice,
relatedItems: [item1]
};
// Create circular reference
item1.relatedItems.push(item2);
invoice.items.push(item1, item2);
invoice.parent = invoice; // Self-reference
try {
// Try to serialize/process the circular structure
const result = await einvoice.processInvoiceObject(invoice, {
detectCycles: true,
maxTraversalDepth: 100
});
return {
handled: true,
cyclesDetected: result?.cyclesFound || false,
serializable: result?.canSerialize || false,
method: result?.handlingMethod
};
} catch (error) {
return {
handled: false,
error: error.message,
isCircularError: error.message.includes('circular') ||
error.message.includes('Converting circular structure')
};
}
}
);
t.ok(objectGraphCycles.handled || objectGraphCycles.isCircularError,
'Object graph cycles were handled');
// Test 5: Namespace circular dependencies
const namespaceCirularDeps = await performanceTracker.measureAsync(
'namespace-circular-dependencies',
async () => {
const circularNamespaceXML = `<?xml version="1.0" encoding="UTF-8"?>
<ns1:Invoice xmlns:ns1="http://example.com/ns1"
xmlns:ns2="http://example.com/ns2"
xmlns:ns3="http://example.com/ns3">
<ns1:Items>
<ns2:Item ns3:ref="item1">
<ns1:SubItem ns2:ref="item2"/>
</ns2:Item>
<ns3:Item ns1:ref="item2">
<ns2:SubItem ns3:ref="item3"/>
</ns3:Item>
<ns1:Item ns2:ref="item3">
<ns3:SubItem ns1:ref="item1"/>
</ns1:Item>
</ns1:Items>
</ns1:Invoice>`;
try {
const parsed = await einvoice.parseXML(circularNamespaceXML);
const analysis = await einvoice.analyzeNamespaceUsage(parsed);
return {
parsed: true,
namespaceCount: analysis?.namespaces?.length || 0,
hasCrossReferences: analysis?.hasCrossNamespaceRefs || false,
complexityScore: analysis?.complexityScore || 0
};
} catch (error) {
return {
parsed: false,
error: error.message
};
}
}
);
t.ok(namespaceCirularDeps.parsed || namespaceCirularDeps.error,
'Namespace circular dependencies were processed');
// Test 6: Include/Import cycles in documents
const includeImportCycles = await performanceTracker.measureAsync(
'include-import-cycles',
async () => {
const documents = {
'main.xml': `<?xml version="1.0"?>
<Invoice xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part1.xml"/>
</Invoice>`,
'part1.xml': `<?xml version="1.0"?>
<InvoicePart xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="part2.xml"/>
</InvoicePart>`,
'part2.xml': `<?xml version="1.0"?>
<InvoicePart xmlns:xi="http://www.w3.org/2001/XInclude">
<xi:include href="main.xml"/>
</InvoicePart>`
};
try {
const result = await einvoice.processWithIncludes(documents['main.xml'], {
resolveIncludes: true,
maxIncludeDepth: 10,
includeMap: documents
});
return {
processed: true,
includeDepthReached: result?.maxDepthReached || false,
cycleDetected: result?.includeCycleDetected || false
};
} catch (error) {
return {
processed: false,
error: error.message,
isIncludeError: error.message.includes('include') || error.message.includes('XInclude')
};
}
}
);
t.ok(includeImportCycles.processed || includeImportCycles.isIncludeError,
'Include cycles were handled');
// Test 7: Circular parent-child relationships
const parentChildCircular = await performanceTracker.measureAsync(
'parent-child-circular',
async () => {
// Test various parent-child circular scenarios
const scenarios = [
{
name: 'self-parent',
xml: `<Invoice id="inv1" parent="inv1"><ID>001</ID></Invoice>`
},
{
name: 'mutual-parents',
xml: `<Invoices>
<Invoice id="inv1" parent="inv2"><ID>001</ID></Invoice>
<Invoice id="inv2" parent="inv1"><ID>002</ID></Invoice>
</Invoices>`
},
{
name: 'chain-loop',
xml: `<Invoices>
<Invoice id="A" parent="B"><ID>A</ID></Invoice>
<Invoice id="B" parent="C"><ID>B</ID></Invoice>
<Invoice id="C" parent="A"><ID>C</ID></Invoice>
</Invoices>`
}
];
const results = [];
for (const scenario of scenarios) {
try {
const parsed = await einvoice.parseXML(scenario.xml);
const hierarchy = await einvoice.buildHierarchy(parsed, {
detectCircular: true
});
results.push({
scenario: scenario.name,
handled: true,
isCircular: hierarchy?.hasCircularParentage || false,
maxDepth: hierarchy?.maxDepth || 0
});
} catch (error) {
results.push({
scenario: scenario.name,
handled: false,
error: error.message
});
}
}
return results;
}
);
parentChildCircular.forEach(result => {
t.ok(result.handled || result.error,
`Parent-child circular scenario ${result.scenario} was processed`);
});
// Test 8: Circular calculations
const circularCalculations = await performanceTracker.measureAsync(
'circular-calculations',
async () => {
const calculationXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<Calculations>
<Field name="subtotal" formula="=total-tax"/>
<Field name="tax" formula="=subtotal*0.2"/>
<Field name="total" formula="=subtotal+tax"/>
</Calculations>
<Items>
<Item price="100" quantity="2"/>
<Item price="50" quantity="3"/>
</Items>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(calculationXML);
const calculated = await einvoice.evaluateCalculations(parsed, {
maxIterations: 10,
detectCircular: true
});
return {
evaluated: true,
hasCircularDependency: calculated?.circularDependency || false,
resolvedValues: calculated?.resolved || {},
iterations: calculated?.iterationsUsed || 0
};
} catch (error) {
return {
evaluated: false,
error: error.message,
isCircularCalc: error.message.includes('circular') && error.message.includes('calculation')
};
}
}
);
t.ok(circularCalculations.evaluated || circularCalculations.isCircularCalc,
'Circular calculations were handled');
// Test 9: Memory safety with circular structures
const memorySafetyCircular = await performanceTracker.measureAsync(
'memory-safety-circular',
async () => {
const startMemory = process.memoryUsage();
// Create a deeply circular structure
const createCircularChain = (depth: number) => {
const nodes = [];
for (let i = 0; i < depth; i++) {
nodes.push({ id: i, next: null, data: 'X'.repeat(1000) });
}
// Link them circularly
for (let i = 0; i < depth; i++) {
nodes[i].next = nodes[(i + 1) % depth];
}
return nodes[0];
};
const results = {
smallCircle: false,
mediumCircle: false,
largeCircle: false,
memoryStable: true
};
try {
// Test increasingly large circular structures
const small = createCircularChain(10);
await einvoice.processCircularStructure(small);
results.smallCircle = true;
const medium = createCircularChain(100);
await einvoice.processCircularStructure(medium);
results.mediumCircle = true;
const large = createCircularChain(1000);
await einvoice.processCircularStructure(large);
results.largeCircle = true;
const endMemory = process.memoryUsage();
const memoryIncrease = endMemory.heapUsed - startMemory.heapUsed;
results.memoryStable = memoryIncrease < 100 * 1024 * 1024; // Less than 100MB
} catch (error) {
// Expected for very large structures
}
return results;
}
);
t.ok(memorySafetyCircular.smallCircle, 'Small circular structures handled safely');
t.ok(memorySafetyCircular.memoryStable, 'Memory usage remained stable');
// Test 10: Format conversion with circular references
const formatConversionCircular = await performanceTracker.measureAsync(
'format-conversion-circular',
async () => {
// Create UBL invoice with circular references
const ublWithCircular = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CIRC-001</ID>
<InvoiceReference>
<ID>CIRC-001</ID> <!-- Self-reference -->
</InvoiceReference>
<OrderReference>
<DocumentReference>
<ID>ORDER-001</ID>
<IssuerParty>
<PartyReference>
<ID>CIRC-001</ID> <!-- Circular reference back to invoice -->
</PartyReference>
</IssuerParty>
</DocumentReference>
</OrderReference>
</Invoice>`;
try {
// Convert to CII
const converted = await einvoice.convertFormat(ublWithCircular, 'cii', {
handleCircularRefs: true,
maxRefDepth: 5
});
// Check if circular refs were handled
const analysis = await einvoice.analyzeReferences(converted);
return {
converted: true,
circularRefsPreserved: analysis?.hasCircularRefs || false,
refsFlattened: analysis?.refsFlattened || false,
conversionMethod: analysis?.method
};
} catch (error) {
return {
converted: false,
error: error.message
};
}
}
);
t.ok(formatConversionCircular.converted || formatConversionCircular.error,
'Format conversion with circular refs was handled');
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();