update
This commit is contained in:
@ -0,0 +1,540 @@
|
||||
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();
|
Reference in New Issue
Block a user