540 lines
16 KiB
TypeScript
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(); |