This commit is contained in:
2025-05-26 04:04:51 +00:00
parent 39942638d9
commit 1d52ce1211
23 changed files with 13545 additions and 4 deletions

View File

@ -0,0 +1,461 @@
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-01: Empty Invoice Files');
tap.test('EDGE-01: Empty Invoice Files - should handle empty and near-empty files gracefully', async (t) => {
const einvoice = new EInvoice();
// Test 1: Completely empty file
const completelyEmpty = await performanceTracker.measureAsync(
'completely-empty-file',
async () => {
const emptyContent = '';
try {
const result = await einvoice.parseDocument(emptyContent);
return {
handled: true,
parsed: !!result,
error: null,
contentLength: emptyContent.length
};
} catch (error) {
return {
handled: true,
parsed: false,
error: error.message,
errorType: error.constructor.name
};
}
}
);
t.ok(completelyEmpty.handled, 'Completely empty file was handled');
t.notOk(completelyEmpty.parsed, 'Empty file was not parsed as valid');
// Test 2: Only whitespace
const onlyWhitespace = await performanceTracker.measureAsync(
'only-whitespace',
async () => {
const whitespaceVariants = [
' ',
'\n',
'\r\n',
'\t',
' \n\n\t\t \r\n ',
' '.repeat(1000)
];
const results = [];
for (const content of whitespaceVariants) {
try {
const result = await einvoice.parseDocument(content);
results.push({
content: content.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'),
length: content.length,
parsed: !!result,
error: null
});
} catch (error) {
results.push({
content: content.replace(/\n/g, '\\n').replace(/\r/g, '\\r').replace(/\t/g, '\\t'),
length: content.length,
parsed: false,
error: error.message
});
}
}
return results;
}
);
onlyWhitespace.forEach(result => {
t.notOk(result.parsed, `Whitespace-only content not parsed: "${result.content}"`);
});
// Test 3: Empty XML structure
const emptyXMLStructure = await performanceTracker.measureAsync(
'empty-xml-structure',
async () => {
const emptyStructures = [
'<?xml version="1.0" encoding="UTF-8"?>',
'<?xml version="1.0" encoding="UTF-8"?>\n',
'<?xml version="1.0" encoding="UTF-8"?><Invoice></Invoice>',
'<?xml version="1.0" encoding="UTF-8"?><Invoice/>',
'<Invoice></Invoice>',
'<Invoice/>'
];
const results = [];
for (const xml of emptyStructures) {
try {
const result = await einvoice.parseDocument(xml);
const validation = await einvoice.validate(result);
results.push({
xml: xml.substring(0, 50),
parsed: true,
valid: validation?.isValid || false,
hasContent: !!result && Object.keys(result).length > 0
});
} catch (error) {
results.push({
xml: xml.substring(0, 50),
parsed: false,
error: error.message
});
}
}
return results;
}
);
emptyXMLStructure.forEach(result => {
if (result.parsed) {
t.notOk(result.valid, 'Empty XML structure is not valid invoice');
}
});
// Test 4: Empty required fields
const emptyRequiredFields = await performanceTracker.measureAsync(
'empty-required-fields',
async () => {
const testCases = [
{
name: 'empty-id',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID></ID>
<IssueDate>2024-01-01</IssueDate>
</Invoice>`
},
{
name: 'whitespace-id',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID> </ID>
<IssueDate>2024-01-01</IssueDate>
</Invoice>`
},
{
name: 'empty-amount',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>INV-001</ID>
<TotalAmount></TotalAmount>
</Invoice>`
}
];
const results = [];
for (const testCase of testCases) {
try {
const parsed = await einvoice.parseDocument(testCase.xml);
const validation = await einvoice.validate(parsed);
results.push({
name: testCase.name,
parsed: true,
valid: validation?.isValid || false,
errors: validation?.errors || []
});
} catch (error) {
results.push({
name: testCase.name,
parsed: false,
error: error.message
});
}
}
return results;
}
);
emptyRequiredFields.forEach(result => {
t.notOk(result.valid, `${result.name} is not valid`);
});
// Test 5: Zero-byte file
const zeroByteFile = await performanceTracker.measureAsync(
'zero-byte-file',
async () => {
const zeroByteBuffer = Buffer.alloc(0);
try {
const result = await einvoice.parseDocument(zeroByteBuffer);
return {
handled: true,
parsed: !!result,
bufferLength: zeroByteBuffer.length
};
} catch (error) {
return {
handled: true,
parsed: false,
error: error.message,
bufferLength: zeroByteBuffer.length
};
}
}
);
t.ok(zeroByteFile.handled, 'Zero-byte buffer was handled');
t.equal(zeroByteFile.bufferLength, 0, 'Buffer length is zero');
// Test 6: Empty arrays and objects
const emptyCollections = await performanceTracker.measureAsync(
'empty-collections',
async () => {
const testCases = [
{
name: 'empty-line-items',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>INV-001</ID>
<InvoiceLines></InvoiceLines>
</Invoice>`
},
{
name: 'empty-tax-totals',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>INV-001</ID>
<TaxTotal></TaxTotal>
</Invoice>`
}
];
const results = [];
for (const testCase of testCases) {
try {
const parsed = await einvoice.parseDocument(testCase.xml);
results.push({
name: testCase.name,
parsed: true,
hasEmptyCollections: true,
structure: JSON.stringify(parsed).substring(0, 100)
});
} catch (error) {
results.push({
name: testCase.name,
parsed: false,
error: error.message
});
}
}
return results;
}
);
emptyCollections.forEach(result => {
t.ok(result.parsed || result.error, `${result.name} was processed`);
});
// Test 7: Empty PDF files
const emptyPDFFiles = await performanceTracker.measureAsync(
'empty-pdf-files',
async () => {
const pdfTests = [
{
name: 'empty-pdf-header',
content: Buffer.from('%PDF-1.4\n%%EOF')
},
{
name: 'pdf-no-content',
content: Buffer.from('%PDF-1.4\n1 0 obj\n<<>>\nendobj\nxref\n0 1\n0000000000 65535 f\ntrailer\n<</Size 1>>\n%%EOF')
},
{
name: 'zero-byte-pdf',
content: Buffer.alloc(0)
}
];
const results = [];
for (const test of pdfTests) {
try {
const result = await einvoice.extractFromPDF(test.content);
results.push({
name: test.name,
processed: true,
hasXML: !!result?.xml,
hasAttachments: result?.attachments?.length > 0,
size: test.content.length
});
} catch (error) {
results.push({
name: test.name,
processed: false,
error: error.message,
size: test.content.length
});
}
}
return results;
}
);
emptyPDFFiles.forEach(result => {
t.ok(!result.hasXML, `${result.name} has no XML content`);
});
// Test 8: Format detection on empty files
const formatDetectionEmpty = await performanceTracker.measureAsync(
'format-detection-empty',
async () => {
const emptyVariants = [
{ content: '', name: 'empty-string' },
{ content: ' ', name: 'space' },
{ content: '\n', name: 'newline' },
{ content: '<?xml?>', name: 'incomplete-xml-declaration' },
{ content: '<', name: 'single-bracket' },
{ content: Buffer.alloc(0), name: 'empty-buffer' }
];
const results = [];
for (const variant of emptyVariants) {
try {
const format = await einvoice.detectFormat(variant.content);
results.push({
name: variant.name,
detected: !!format,
format: format,
confidence: format?.confidence || 0
});
} catch (error) {
results.push({
name: variant.name,
detected: false,
error: error.message
});
}
}
return results;
}
);
formatDetectionEmpty.forEach(result => {
t.notOk(result.detected, `Format not detected for ${result.name}`);
});
// Test 9: Empty namespace handling
const emptyNamespaces = await performanceTracker.measureAsync(
'empty-namespace-handling',
async () => {
const namespaceTests = [
{
name: 'empty-default-namespace',
xml: '<Invoice xmlns=""></Invoice>'
},
{
name: 'empty-prefix-namespace',
xml: '<ns:Invoice xmlns:ns=""></ns:Invoice>'
},
{
name: 'whitespace-namespace',
xml: '<Invoice xmlns=" "></Invoice>'
}
];
const results = [];
for (const test of namespaceTests) {
try {
const parsed = await einvoice.parseDocument(test.xml);
results.push({
name: test.name,
parsed: true,
hasNamespace: !!parsed?.namespace
});
} catch (error) {
results.push({
name: test.name,
parsed: false,
error: error.message
});
}
}
return results;
}
);
emptyNamespaces.forEach(result => {
t.ok(result.parsed !== undefined, `${result.name} was processed`);
});
// Test 10: Recovery from empty files
const emptyFileRecovery = await performanceTracker.measureAsync(
'empty-file-recovery',
async () => {
const recoveryTest = async () => {
const results = {
emptyHandled: false,
normalAfterEmpty: false,
batchWithEmpty: false
};
// Test 1: Handle empty file
try {
await einvoice.parseDocument('');
} catch (error) {
results.emptyHandled = true;
}
// Test 2: Parse normal file after empty
try {
const normal = await einvoice.parseDocument(
'<?xml version="1.0"?><Invoice><ID>TEST</ID></Invoice>'
);
results.normalAfterEmpty = !!normal;
} catch (error) {
// Should not happen
}
// Test 3: Batch with empty file
try {
const batch = await einvoice.batchProcess([
'<?xml version="1.0"?><Invoice><ID>1</ID></Invoice>',
'',
'<?xml version="1.0"?><Invoice><ID>2</ID></Invoice>'
]);
results.batchWithEmpty = batch?.processed === 2;
} catch (error) {
// Batch might fail completely
}
return results;
};
return await recoveryTest();
}
);
t.ok(emptyFileRecovery.normalAfterEmpty, 'Can parse normal file after empty file');
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();

View File

@ -0,0 +1,668 @@
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';
import * as fs from 'fs';
import * as path from 'path';
const performanceTracker = new PerformanceTracker('EDGE-02: Gigabyte-Size Invoices');
tap.test('EDGE-02: Gigabyte-Size Invoices - should handle extremely large invoice files', async (t) => {
const einvoice = new EInvoice();
// Test 1: Large number of line items
const manyLineItems = await performanceTracker.measureAsync(
'many-line-items',
async () => {
// Create invoice with 100,000 line items (simulated)
const lineItemCount = 100000;
const chunkSize = 1000;
const header = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>LARGE-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceLines>`;
const footer = ` </InvoiceLines>
<TotalAmount>1000000.00</TotalAmount>
</Invoice>`;
// Simulate streaming parse
const startTime = Date.now();
const startMemory = process.memoryUsage();
try {
// In real implementation, would stream parse
const mockStream = {
header,
lineItemCount,
footer,
processed: 0
};
// Process in chunks
while (mockStream.processed < lineItemCount) {
const batchSize = Math.min(chunkSize, lineItemCount - mockStream.processed);
// Simulate processing chunk
for (let i = 0; i < batchSize; i++) {
const itemNum = mockStream.processed + i;
// Would normally append to stream: generateLineItem(itemNum)
}
mockStream.processed += batchSize;
// Check memory usage
const currentMemory = process.memoryUsage();
if (currentMemory.heapUsed - startMemory.heapUsed > 500 * 1024 * 1024) {
throw new Error('Memory limit exceeded');
}
}
const endTime = Date.now();
const endMemory = process.memoryUsage();
return {
success: true,
lineItems: lineItemCount,
timeTaken: endTime - startTime,
memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
throughput: lineItemCount / ((endTime - startTime) / 1000)
};
} catch (error) {
return {
success: false,
error: error.message,
lineItems: mockStream?.processed || 0
};
}
}
);
t.ok(manyLineItems.success || manyLineItems.error, 'Large line item count was processed');
// Test 2: Large text content
const largeTextContent = await performanceTracker.measureAsync(
'large-text-content',
async () => {
// Create invoice with very large description fields
const descriptionSize = 10 * 1024 * 1024; // 10MB per description
const itemCount = 10;
const results = {
totalSize: 0,
processed: 0,
memoryPeaks: []
};
try {
for (let i = 0; i < itemCount; i++) {
const largeDescription = 'A'.repeat(descriptionSize);
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>LARGE-TEXT-${i}</ID>
<Description>${largeDescription}</Description>
</Invoice>`;
const memBefore = process.memoryUsage().heapUsed;
// Process with streaming if available
const processed = await einvoice.parseWithStreaming(xml);
const memAfter = process.memoryUsage().heapUsed;
results.memoryPeaks.push(memAfter - memBefore);
results.totalSize += xml.length;
results.processed++;
// Force GC between items if available
if (global.gc) {
global.gc();
}
}
return {
success: true,
...results,
avgMemoryPerItem: results.memoryPeaks.reduce((a, b) => a + b, 0) / results.memoryPeaks.length
};
} catch (error) {
return {
success: false,
error: error.message,
...results
};
}
}
);
t.ok(largeTextContent.processed > 0, 'Large text content was processed');
// Test 3: Streaming vs loading entire file
const streamingComparison = await performanceTracker.measureAsync(
'streaming-vs-loading',
async () => {
const testSizes = [
{ size: 1 * 1024 * 1024, name: '1MB' },
{ size: 10 * 1024 * 1024, name: '10MB' },
{ size: 100 * 1024 * 1024, name: '100MB' }
];
const results = [];
for (const test of testSizes) {
// Generate test data
const testXML = generateLargeInvoice(test.size);
// Test full loading
let fullLoadResult;
try {
const startTime = Date.now();
const startMem = process.memoryUsage();
await einvoice.parseDocument(testXML);
const endTime = Date.now();
const endMem = process.memoryUsage();
fullLoadResult = {
method: 'full-load',
success: true,
time: endTime - startTime,
memory: endMem.heapUsed - startMem.heapUsed
};
} catch (error) {
fullLoadResult = {
method: 'full-load',
success: false,
error: error.message
};
}
// Test streaming
let streamResult;
try {
const startTime = Date.now();
const startMem = process.memoryUsage();
await einvoice.parseWithStreaming(testXML);
const endTime = Date.now();
const endMem = process.memoryUsage();
streamResult = {
method: 'streaming',
success: true,
time: endTime - startTime,
memory: endMem.heapUsed - startMem.heapUsed
};
} catch (error) {
streamResult = {
method: 'streaming',
success: false,
error: error.message
};
}
results.push({
size: test.name,
fullLoad: fullLoadResult,
streaming: streamResult,
memoryRatio: streamResult.memory && fullLoadResult.memory ?
streamResult.memory / fullLoadResult.memory : null
});
}
return results;
}
);
streamingComparison.forEach(result => {
if (result.streaming.success && result.fullLoad.success) {
t.ok(result.memoryRatio < 0.5,
`Streaming uses less memory for ${result.size}`);
}
});
// Test 4: Memory-mapped file processing
const memoryMappedProcessing = await performanceTracker.measureAsync(
'memory-mapped-processing',
async () => {
const testFile = path.join(process.cwd(), '.nogit', 'large-test.xml');
const fileSize = 500 * 1024 * 1024; // 500MB
try {
// Create large test file if it doesn't exist
if (!fs.existsSync(testFile)) {
const dir = path.dirname(testFile);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
// Write file in chunks
const stream = fs.createWriteStream(testFile);
stream.write('<?xml version="1.0" encoding="UTF-8"?><Invoice><Items>');
const chunkSize = 1024 * 1024; // 1MB chunks
const chunk = '<Item>' + 'X'.repeat(chunkSize - 14) + '</Item>';
const chunks = Math.floor(fileSize / chunkSize);
for (let i = 0; i < chunks; i++) {
stream.write(chunk);
}
stream.write('</Items></Invoice>');
stream.end();
}
// Process with memory mapping
const startTime = Date.now();
const startMem = process.memoryUsage();
const result = await einvoice.processLargeFile(testFile, {
useMemoryMapping: true,
chunkSize: 10 * 1024 * 1024 // 10MB chunks
});
const endTime = Date.now();
const endMem = process.memoryUsage();
// Clean up
if (fs.existsSync(testFile)) {
fs.unlinkSync(testFile);
}
return {
success: true,
fileSize,
timeTaken: endTime - startTime,
memoryUsed: endMem.heapUsed - startMem.heapUsed,
throughputMBps: (fileSize / (1024 * 1024)) / ((endTime - startTime) / 1000)
};
} catch (error) {
// Clean up on error
if (fs.existsSync(testFile)) {
fs.unlinkSync(testFile);
}
return {
success: false,
error: error.message
};
}
}
);
t.ok(memoryMappedProcessing.success || memoryMappedProcessing.error,
'Memory-mapped processing completed');
// Test 5: Concurrent large file processing
const concurrentLargeFiles = await performanceTracker.measureAsync(
'concurrent-large-files',
async () => {
const fileCount = 5;
const fileSize = 50 * 1024 * 1024; // 50MB each
const promises = [];
const startTime = Date.now();
const startMem = process.memoryUsage();
for (let i = 0; i < fileCount; i++) {
const xml = generateLargeInvoice(fileSize);
promises.push(
einvoice.parseWithStreaming(xml)
.then(() => ({ fileId: i, success: true }))
.catch(error => ({ fileId: i, success: false, error: error.message }))
);
}
const results = await Promise.all(promises);
const endTime = Date.now();
const endMem = process.memoryUsage();
const successful = results.filter(r => r.success).length;
return {
totalFiles: fileCount,
successful,
failed: fileCount - successful,
totalTime: endTime - startTime,
totalMemory: endMem.heapUsed - startMem.heapUsed,
avgTimePerFile: (endTime - startTime) / fileCount,
results
};
}
);
t.ok(concurrentLargeFiles.successful > 0, 'Some concurrent large files were processed');
// Test 6: Progressive loading with backpressure
const progressiveLoading = await performanceTracker.measureAsync(
'progressive-loading-backpressure',
async () => {
const totalSize = 200 * 1024 * 1024; // 200MB
const chunkSize = 10 * 1024 * 1024; // 10MB chunks
const results = {
chunksProcessed: 0,
backpressureEvents: 0,
memoryPeaks: [],
processingTimes: []
};
try {
for (let offset = 0; offset < totalSize; offset += chunkSize) {
const chunkData = generateInvoiceChunk(offset, Math.min(chunkSize, totalSize - offset));
const chunkStart = Date.now();
const memBefore = process.memoryUsage();
// Check for backpressure
if (memBefore.heapUsed > 300 * 1024 * 1024) {
results.backpressureEvents++;
// Wait for memory to reduce
if (global.gc) {
global.gc();
}
await new Promise(resolve => setTimeout(resolve, 100));
}
await einvoice.processChunk(chunkData, {
isFirst: offset === 0,
isLast: offset + chunkSize >= totalSize
});
const chunkEnd = Date.now();
const memAfter = process.memoryUsage();
results.chunksProcessed++;
results.processingTimes.push(chunkEnd - chunkStart);
results.memoryPeaks.push(memAfter.heapUsed);
}
return {
success: true,
...results,
avgProcessingTime: results.processingTimes.reduce((a, b) => a + b, 0) / results.processingTimes.length,
maxMemoryPeak: Math.max(...results.memoryPeaks)
};
} catch (error) {
return {
success: false,
error: error.message,
...results
};
}
}
);
t.ok(progressiveLoading.chunksProcessed > 0, 'Progressive loading processed chunks');
t.ok(progressiveLoading.backpressureEvents >= 0, 'Backpressure was handled');
// Test 7: Large attachment handling
const largeAttachments = await performanceTracker.measureAsync(
'large-attachment-handling',
async () => {
const attachmentSizes = [
{ size: 10 * 1024 * 1024, name: '10MB' },
{ size: 50 * 1024 * 1024, name: '50MB' },
{ size: 100 * 1024 * 1024, name: '100MB' }
];
const results = [];
for (const attachment of attachmentSizes) {
try {
// Create PDF with large attachment
const largePDF = createPDFWithAttachment(attachment.size);
const startTime = Date.now();
const startMem = process.memoryUsage();
const extracted = await einvoice.extractFromPDF(largePDF, {
streamAttachments: true
});
const endTime = Date.now();
const endMem = process.memoryUsage();
results.push({
size: attachment.name,
success: true,
hasAttachment: !!extracted?.attachments?.length,
timeTaken: endTime - startTime,
memoryUsed: endMem.heapUsed - startMem.heapUsed
});
} catch (error) {
results.push({
size: attachment.name,
success: false,
error: error.message
});
}
}
return results;
}
);
largeAttachments.forEach(result => {
t.ok(result.success || result.error, `${result.size} attachment was processed`);
});
// Test 8: Format conversion of large files
const largeFormatConversion = await performanceTracker.measureAsync(
'large-format-conversion',
async () => {
const testSizes = [10, 50]; // MB
const results = [];
for (const sizeMB of testSizes) {
const size = sizeMB * 1024 * 1024;
const largeUBL = generateLargeUBLInvoice(size);
try {
const startTime = Date.now();
const startMem = process.memoryUsage();
const converted = await einvoice.convertFormat(largeUBL, 'cii', {
streaming: true
});
const endTime = Date.now();
const endMem = process.memoryUsage();
results.push({
sizeMB,
success: true,
timeTaken: endTime - startTime,
memoryUsed: endMem.heapUsed - startMem.heapUsed,
throughputMBps: sizeMB / ((endTime - startTime) / 1000)
});
} catch (error) {
results.push({
sizeMB,
success: false,
error: error.message
});
}
}
return results;
}
);
largeFormatConversion.forEach(result => {
t.ok(result.success || result.error, `${result.sizeMB}MB conversion completed`);
});
// Test 9: Validation of gigabyte files
const gigabyteValidation = await performanceTracker.measureAsync(
'gigabyte-file-validation',
async () => {
// Simulate validation of 1GB file
const fileSize = 1024 * 1024 * 1024; // 1GB
const chunkSize = 50 * 1024 * 1024; // 50MB chunks
const validationResults = {
chunksValidated: 0,
errors: [],
warnings: [],
timeTaken: 0
};
const startTime = Date.now();
try {
const totalChunks = Math.ceil(fileSize / chunkSize);
for (let i = 0; i < totalChunks; i++) {
// Simulate chunk validation
const chunkValidation = await einvoice.validateChunk({
chunkIndex: i,
totalChunks,
size: Math.min(chunkSize, fileSize - i * chunkSize)
});
validationResults.chunksValidated++;
if (chunkValidation?.errors) {
validationResults.errors.push(...chunkValidation.errors);
}
if (chunkValidation?.warnings) {
validationResults.warnings.push(...chunkValidation.warnings);
}
// Simulate memory pressure
if (i % 5 === 0 && global.gc) {
global.gc();
}
}
validationResults.timeTaken = Date.now() - startTime;
return {
success: true,
...validationResults,
throughputMBps: (fileSize / (1024 * 1024)) / (validationResults.timeTaken / 1000)
};
} catch (error) {
return {
success: false,
error: error.message,
...validationResults
};
}
}
);
t.ok(gigabyteValidation.chunksValidated > 0, 'Gigabyte file validation progressed');
// Test 10: Recovery after large file processing
const largeFileRecovery = await performanceTracker.measureAsync(
'large-file-recovery',
async () => {
const results = {
largeFileProcessed: false,
memoryRecovered: false,
normalFileAfter: false
};
// Get baseline memory
if (global.gc) global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
const baselineMemory = process.memoryUsage().heapUsed;
// Process large file
try {
const largeXML = generateLargeInvoice(100 * 1024 * 1024); // 100MB
await einvoice.parseDocument(largeXML);
results.largeFileProcessed = true;
} catch (error) {
// Expected for very large files
}
// Force cleanup
if (global.gc) global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
const afterCleanupMemory = process.memoryUsage().heapUsed;
results.memoryRecovered = afterCleanupMemory < baselineMemory + 50 * 1024 * 1024; // Within 50MB
// Try normal file
try {
const normalXML = '<?xml version="1.0"?><Invoice><ID>NORMAL</ID></Invoice>';
await einvoice.parseDocument(normalXML);
results.normalFileAfter = true;
} catch (error) {
// Should not happen
}
return results;
}
);
t.ok(largeFileRecovery.memoryRecovered, 'Memory was recovered after large file');
t.ok(largeFileRecovery.normalFileAfter, 'Normal processing works after large file');
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to generate large invoice
function generateLargeInvoice(targetSize: number): string {
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice><Items>';
const itemTemplate = '<Item><ID>XXX</ID><Description>Test item description that contains some text</Description><Amount>100.00</Amount></Item>';
const itemSize = itemTemplate.length;
const itemCount = Math.floor(targetSize / itemSize);
for (let i = 0; i < itemCount; i++) {
xml += itemTemplate.replace('XXX', i.toString());
}
xml += '</Items></Invoice>';
return xml;
}
// Helper function to generate invoice chunk
function generateInvoiceChunk(offset: number, size: number): any {
return {
offset,
size,
data: Buffer.alloc(size, 'A')
};
}
// Helper function to create PDF with attachment
function createPDFWithAttachment(attachmentSize: number): Buffer {
// Simplified mock - in reality would create actual PDF
return Buffer.alloc(attachmentSize + 1024, 'P');
}
// Helper function to generate large UBL invoice
function generateLargeUBLInvoice(size: number): string {
let xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>LARGE-UBL-001</ID>
<IssueDate>2024-01-01</IssueDate>
<InvoiceLines>`;
const lineTemplate = `<InvoiceLine><ID>X</ID><InvoicedQuantity>1</InvoicedQuantity><LineExtensionAmount>100</LineExtensionAmount></InvoiceLine>`;
const lineSize = lineTemplate.length;
const lineCount = Math.floor(size / lineSize);
for (let i = 0; i < lineCount; i++) {
xml += lineTemplate.replace('X', i.toString());
}
xml += '</InvoiceLines></Invoice>';
return xml;
}
// Run the test
tap.start();

View File

@ -0,0 +1,651 @@
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-03: Deeply Nested XML Structures');
tap.test('EDGE-03: Deeply Nested XML Structures - should handle extremely nested XML', async (t) => {
const einvoice = new EInvoice();
// Test 1: Linear deep nesting
const linearDeepNesting = await performanceTracker.measureAsync(
'linear-deep-nesting',
async () => {
const testDepths = [10, 100, 1000, 5000, 10000];
const results = [];
for (const depth of testDepths) {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
// Build deeply nested structure
for (let i = 0; i < depth; i++) {
xml += ' '.repeat(i) + `<Level${i}>\n`;
}
xml += ' '.repeat(depth) + '<Data>Invoice Data</Data>\n';
// Close all tags
for (let i = depth - 1; i >= 0; i--) {
xml += ' '.repeat(i) + `</Level${i}>\n`;
}
const startTime = Date.now();
const startMemory = process.memoryUsage();
try {
const result = await einvoice.parseXML(xml);
const endTime = Date.now();
const endMemory = process.memoryUsage();
results.push({
depth,
success: true,
timeTaken: endTime - startTime,
memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
hasData: !!result
});
} catch (error) {
results.push({
depth,
success: false,
error: error.message,
isStackOverflow: error.message.includes('stack') || error.message.includes('depth')
});
}
}
return results;
}
);
linearDeepNesting.forEach(result => {
if (result.depth <= 1000) {
t.ok(result.success, `Depth ${result.depth} should be handled`);
} else {
t.ok(!result.success || result.isStackOverflow, `Extreme depth ${result.depth} should be limited`);
}
});
// Test 2: Recursive element nesting
const recursiveElementNesting = await performanceTracker.measureAsync(
'recursive-element-nesting',
async () => {
const createRecursiveStructure = (depth: number): string => {
if (depth === 0) {
return '<Amount>100.00</Amount>';
}
return `<Item>
<ID>ITEM-${depth}</ID>
<SubItems>
${createRecursiveStructure(depth - 1)}
</SubItems>
</Item>`;
};
const testDepths = [5, 10, 20, 50];
const results = [];
for (const depth of testDepths) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>RECURSIVE-001</ID>
<Items>
${createRecursiveStructure(depth)}
</Items>
</Invoice>`;
try {
const startTime = Date.now();
const parsed = await einvoice.parseXML(xml);
const endTime = Date.now();
// Count actual depth
let actualDepth = 0;
let current = parsed;
while (current?.Items || current?.SubItems) {
actualDepth++;
current = current.Items || current.SubItems;
}
results.push({
requestedDepth: depth,
actualDepth,
success: true,
timeTaken: endTime - startTime
});
} catch (error) {
results.push({
requestedDepth: depth,
success: false,
error: error.message
});
}
}
return results;
}
);
recursiveElementNesting.forEach(result => {
t.ok(result.success || result.error, `Recursive depth ${result.requestedDepth} was processed`);
});
// Test 3: Namespace nesting complexity
const namespaceNesting = await performanceTracker.measureAsync(
'namespace-nesting-complexity',
async () => {
const createNamespaceNesting = (depth: number): string => {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
// Create nested elements with different namespaces
for (let i = 0; i < depth; i++) {
xml += ' '.repeat(i) + `<ns${i}:Element xmlns:ns${i}="http://example.com/ns${i}">\n`;
}
xml += ' '.repeat(depth) + '<Data>Content</Data>\n';
// Close all namespace elements
for (let i = depth - 1; i >= 0; i--) {
xml += ' '.repeat(i) + `</ns${i}:Element>\n`;
}
return xml;
};
const testDepths = [5, 10, 25, 50, 100];
const results = [];
for (const depth of testDepths) {
const xml = createNamespaceNesting(depth);
try {
const startTime = Date.now();
const parsed = await einvoice.parseXML(xml);
const endTime = Date.now();
results.push({
depth,
success: true,
timeTaken: endTime - startTime,
namespacesPreserved: true // Check if namespaces were preserved
});
} catch (error) {
results.push({
depth,
success: false,
error: error.message
});
}
}
return results;
}
);
namespaceNesting.forEach(result => {
if (result.depth <= 50) {
t.ok(result.success, `Namespace depth ${result.depth} should be handled`);
}
});
// Test 4: Mixed content deep nesting
const mixedContentNesting = await performanceTracker.measureAsync(
'mixed-content-deep-nesting',
async () => {
const createMixedNesting = (depth: number): string => {
let xml = '';
for (let i = 0; i < depth; i++) {
xml += `<Level${i}>Text before `;
}
xml += '<Value>Core Value</Value>';
for (let i = depth - 1; i >= 0; i--) {
xml += ` text after</Level${i}>`;
}
return xml;
};
const testCases = [10, 50, 100, 500];
const results = [];
for (const depth of testCases) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<MixedContent>
${createMixedNesting(depth)}
</MixedContent>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
results.push({
depth,
success: true,
hasMixedContent: true
});
} catch (error) {
results.push({
depth,
success: false,
error: error.message
});
}
}
return results;
}
);
mixedContentNesting.forEach(result => {
t.ok(result.success || result.error, `Mixed content depth ${result.depth} was handled`);
});
// Test 5: Attribute-heavy deep nesting
const attributeHeavyNesting = await performanceTracker.measureAsync(
'attribute-heavy-nesting',
async () => {
const createAttributeNesting = (depth: number, attrsPerLevel: number): string => {
let xml = '';
for (let i = 0; i < depth; i++) {
xml += `<Element${i}`;
// Add multiple attributes at each level
for (let j = 0; j < attrsPerLevel; j++) {
xml += ` attr${j}="value${i}_${j}"`;
}
xml += '>';
}
xml += 'Content';
for (let i = depth - 1; i >= 0; i--) {
xml += `</Element${i}>`;
}
return xml;
};
const testCases = [
{ depth: 10, attrs: 10 },
{ depth: 50, attrs: 5 },
{ depth: 100, attrs: 3 },
{ depth: 500, attrs: 1 }
];
const results = [];
for (const test of testCases) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
${createAttributeNesting(test.depth, test.attrs)}
</Invoice>`;
const startTime = Date.now();
const startMemory = process.memoryUsage();
try {
await einvoice.parseXML(xml);
const endTime = Date.now();
const endMemory = process.memoryUsage();
results.push({
depth: test.depth,
attributesPerLevel: test.attrs,
totalAttributes: test.depth * test.attrs,
success: true,
timeTaken: endTime - startTime,
memoryUsed: endMemory.heapUsed - startMemory.heapUsed
});
} catch (error) {
results.push({
depth: test.depth,
attributesPerLevel: test.attrs,
success: false,
error: error.message
});
}
}
return results;
}
);
attributeHeavyNesting.forEach(result => {
t.ok(result.success || result.error,
`Attribute-heavy nesting (depth: ${result.depth}, attrs: ${result.attributesPerLevel}) was processed`);
});
// Test 6: CDATA section nesting
const cdataNesting = await performanceTracker.measureAsync(
'cdata-section-nesting',
async () => {
const depths = [5, 10, 20, 50];
const results = [];
for (const depth of depths) {
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice>';
// Create nested elements with CDATA
for (let i = 0; i < depth; i++) {
xml += `<Level${i}><![CDATA[Data at level ${i} with <special> characters & symbols]]>`;
}
// Close all elements
for (let i = depth - 1; i >= 0; i--) {
xml += `</Level${i}>`;
}
xml += '</Invoice>';
try {
const parsed = await einvoice.parseXML(xml);
results.push({
depth,
success: true,
cdataPreserved: true
});
} catch (error) {
results.push({
depth,
success: false,
error: error.message
});
}
}
return results;
}
);
cdataNesting.forEach(result => {
t.ok(result.success, `CDATA nesting depth ${result.depth} should be handled`);
});
// Test 7: Processing instruction nesting
const processingInstructionNesting = await performanceTracker.measureAsync(
'processing-instruction-nesting',
async () => {
const createPINesting = (depth: number): string => {
let xml = '<?xml version="1.0" encoding="UTF-8"?>\n';
for (let i = 0; i < depth; i++) {
xml += `<?process-level-${i} instruction="value"?>\n`;
xml += `<Level${i}>\n`;
}
xml += '<Data>Content</Data>\n';
for (let i = depth - 1; i >= 0; i--) {
xml += `</Level${i}>\n`;
}
return xml;
};
const depths = [10, 25, 50];
const results = [];
for (const depth of depths) {
const xml = createPINesting(depth);
try {
const parsed = await einvoice.parseXML(xml);
results.push({
depth,
success: true,
processingInstructionsHandled: true
});
} catch (error) {
results.push({
depth,
success: false,
error: error.message
});
}
}
return results;
}
);
processingInstructionNesting.forEach(result => {
t.ok(result.success, `PI nesting depth ${result.depth} should be handled`);
});
// Test 8: Real invoice format deep structures
const realFormatDeepStructures = await performanceTracker.measureAsync(
'real-format-deep-structures',
async () => {
const formats = ['ubl', 'cii'];
const results = [];
for (const format of formats) {
// Create deeply nested invoice structure
let invoice;
if (format === 'ubl') {
invoice = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>DEEP-UBL-001</ID>
<Note>
<SubNote>
<SubSubNote>
<Content>
<Detail>
<SubDetail>
<Information>Deeply nested note</Information>
</SubDetail>
</Detail>
</Content>
</SubSubNote>
</SubNote>
</Note>
<InvoiceLine>
<Item>
<AdditionalItemProperty>
<Value>
<SubValue>
<Detail>
<SubDetail>
<Information>Deep item property</Information>
</SubDetail>
</Detail>
</SubValue>
</Value>
</AdditionalItemProperty>
</Item>
</InvoiceLine>
</Invoice>`;
} else {
invoice = `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:ExchangedDocument>
<ram:ID>DEEP-CII-001</ram:ID>
<ram:IncludedNote>
<ram:Content>
<ram:SubContent>
<ram:Detail>
<ram:SubDetail>
<ram:Information>Deep CII structure</ram:Information>
</ram:SubDetail>
</ram:Detail>
</ram:SubContent>
</ram:Content>
</ram:IncludedNote>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`;
}
try {
const parsed = await einvoice.parseDocument(invoice);
const validated = await einvoice.validate(parsed);
results.push({
format,
parsed: true,
valid: validated?.isValid || false,
deepStructureSupported: true
});
} catch (error) {
results.push({
format,
parsed: false,
error: error.message
});
}
}
return results;
}
);
realFormatDeepStructures.forEach(result => {
t.ok(result.parsed, `${result.format} deep structure should be parsed`);
});
// Test 9: Stack overflow protection
const stackOverflowProtection = await performanceTracker.measureAsync(
'stack-overflow-protection',
async () => {
const extremeDepths = [10000, 50000, 100000];
const results = [];
for (const depth of extremeDepths) {
// Create extremely deep structure efficiently
const parts = [];
parts.push('<?xml version="1.0" encoding="UTF-8"?>');
// Opening tags
for (let i = 0; i < Math.min(depth, 1000); i++) {
parts.push(`<L${i}>`);
}
parts.push('<Data>Test</Data>');
// Closing tags
for (let i = Math.min(depth - 1, 999); i >= 0; i--) {
parts.push(`</L${i}>`);
}
const xml = parts.join('');
const startTime = Date.now();
try {
await einvoice.parseXML(xml, { maxDepth: 1000 });
const endTime = Date.now();
results.push({
depth,
protected: true,
method: 'depth-limit',
timeTaken: endTime - startTime
});
} catch (error) {
const endTime = Date.now();
results.push({
depth,
protected: true,
method: error.message.includes('depth') ? 'depth-check' : 'stack-guard',
timeTaken: endTime - startTime,
error: error.message
});
}
}
return results;
}
);
stackOverflowProtection.forEach(result => {
t.ok(result.protected, `Stack overflow protection active for depth ${result.depth}`);
});
// Test 10: Performance impact of nesting
const nestingPerformanceImpact = await performanceTracker.measureAsync(
'nesting-performance-impact',
async () => {
const depths = [1, 10, 50, 100, 500, 1000];
const results = [];
for (const depth of depths) {
// Create invoice with specific nesting depth
let xml = '<?xml version="1.0" encoding="UTF-8"?><Invoice>';
// Create structure at depth
let current = xml;
for (let i = 0; i < depth; i++) {
current += `<Item${i}>`;
}
current += '<ID>TEST</ID><Amount>100</Amount>';
for (let i = depth - 1; i >= 0; i--) {
current += `</Item${i}>`;
}
current += '</Invoice>';
// Measure parsing time
const iterations = 10;
const times = [];
for (let i = 0; i < iterations; i++) {
const startTime = process.hrtime.bigint();
try {
await einvoice.parseXML(current);
} catch (error) {
// Ignore errors for performance testing
}
const endTime = process.hrtime.bigint();
times.push(Number(endTime - startTime) / 1000000); // Convert to ms
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
const minTime = Math.min(...times);
const maxTime = Math.max(...times);
results.push({
depth,
avgTime,
minTime,
maxTime,
complexity: avgTime / depth // Time per nesting level
});
}
return results;
}
);
// Verify performance doesn't degrade exponentially
const complexities = nestingPerformanceImpact.map(r => r.complexity);
const avgComplexity = complexities.reduce((a, b) => a + b, 0) / complexities.length;
nestingPerformanceImpact.forEach(result => {
t.ok(result.complexity < avgComplexity * 10,
`Nesting depth ${result.depth} has reasonable performance`);
});
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();

View File

@ -0,0 +1,656 @@
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-04: Unusual Character Sets');
tap.test('EDGE-04: Unusual Character Sets - should handle unusual and exotic character encodings', async (t) => {
const einvoice = new EInvoice();
// Test 1: Unicode edge cases
const unicodeEdgeCases = await performanceTracker.measureAsync(
'unicode-edge-cases',
async () => {
const testCases = [
{
name: 'zero-width-characters',
text: 'Invoice\u200B\u200C\u200D\uFEFFNumber',
description: 'Zero-width spaces and joiners'
},
{
name: 'right-to-left',
text: 'مرحبا INV-001 שלום',
description: 'RTL Arabic and Hebrew mixed with LTR'
},
{
name: 'surrogate-pairs',
text: '𝐇𝐞𝐥𝐥𝐨 😀 🎉 Invoice',
description: 'Mathematical bold text and emojis'
},
{
name: 'combining-characters',
text: 'Ińvȯíçë̃ Nüm̈bër̊',
description: 'Combining diacritical marks'
},
{
name: 'control-characters',
text: 'Invoice\x00\x01\x02\x1F\x7FTest',
description: 'Control characters'
},
{
name: 'bidi-override',
text: '\u202Eتسا Invoice 123\u202C',
description: 'Bidirectional override characters'
}
];
const results = [];
for (const testCase of testCases) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>${testCase.text}</ID>
<Description>${testCase.description}</Description>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const idValue = parsed?.ID || '';
results.push({
name: testCase.name,
success: true,
preserved: idValue === testCase.text,
normalized: idValue !== testCase.text,
parsedValue: idValue,
originalLength: testCase.text.length,
parsedLength: idValue.length
});
} catch (error) {
results.push({
name: testCase.name,
success: false,
error: error.message
});
}
}
return results;
}
);
unicodeEdgeCases.forEach(result => {
t.ok(result.success, `Unicode edge case ${result.name} should be handled`);
});
// Test 2: Various character encodings
const characterEncodings = await performanceTracker.measureAsync(
'various-character-encodings',
async () => {
const encodings = [
{
encoding: 'UTF-8',
bom: Buffer.from([0xEF, 0xBB, 0xBF]),
text: 'Übung macht den Meister'
},
{
encoding: 'UTF-16BE',
bom: Buffer.from([0xFE, 0xFF]),
text: 'Invoice \u4E2D\u6587'
},
{
encoding: 'UTF-16LE',
bom: Buffer.from([0xFF, 0xFE]),
text: 'Facture française'
},
{
encoding: 'ISO-8859-1',
bom: null,
text: 'Ñoño español'
},
{
encoding: 'Windows-1252',
bom: null,
text: 'Smart "quotes" and —dashes'
}
];
const results = [];
for (const enc of encodings) {
const xmlContent = `<?xml version="1.0" encoding="${enc.encoding}"?>
<Invoice>
<ID>ENC-001</ID>
<CustomerName>${enc.text}</CustomerName>
</Invoice>`;
try {
// Create buffer with proper encoding
let buffer;
if (enc.bom) {
const textBuffer = Buffer.from(xmlContent, enc.encoding.toLowerCase());
buffer = Buffer.concat([enc.bom, textBuffer]);
} else {
buffer = Buffer.from(xmlContent, enc.encoding.toLowerCase().replace('-', ''));
}
const parsed = await einvoice.parseDocument(buffer);
results.push({
encoding: enc.encoding,
success: true,
hasBOM: !!enc.bom,
textPreserved: parsed?.CustomerName === enc.text
});
} catch (error) {
results.push({
encoding: enc.encoding,
success: false,
error: error.message
});
}
}
return results;
}
);
characterEncodings.forEach(result => {
t.ok(result.success || result.error, `Encoding ${result.encoding} was processed`);
});
// Test 3: Emoji and pictographic characters
const emojiAndPictographs = await performanceTracker.measureAsync(
'emoji-and-pictographs',
async () => {
const emojiTests = [
{
name: 'basic-emoji',
content: 'Invoice 📧 sent ✅'
},
{
name: 'flag-emoji',
content: 'Country: 🇺🇸 🇬🇧 🇩🇪 🇫🇷'
},
{
name: 'skin-tone-emoji',
content: 'Approved by 👍🏻👍🏼👍🏽👍🏾👍🏿'
},
{
name: 'zwj-sequences',
content: 'Family: 👨‍👩‍👧‍👦'
},
{
name: 'mixed-emoji-text',
content: '💰 Total: €1,234.56 💶'
}
];
const results = [];
for (const test of emojiTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>EMOJI-001</ID>
<Note>${test.content}</Note>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const noteValue = parsed?.Note || '';
// Count grapheme clusters (visual characters)
const graphemeCount = [...new Intl.Segmenter().segment(test.content)].length;
const preservedGraphemes = [...new Intl.Segmenter().segment(noteValue)].length;
results.push({
name: test.name,
success: true,
preserved: noteValue === test.content,
originalGraphemes: graphemeCount,
preservedGraphemes,
codePointCount: Array.from(test.content).length,
byteLength: Buffer.from(test.content, 'utf8').length
});
} catch (error) {
results.push({
name: test.name,
success: false,
error: error.message
});
}
}
return results;
}
);
emojiAndPictographs.forEach(result => {
t.ok(result.success, `Emoji test ${result.name} should succeed`);
if (result.success) {
t.ok(result.preserved, `Emoji content should be preserved`);
}
});
// Test 4: Legacy and exotic scripts
const exoticScripts = await performanceTracker.measureAsync(
'exotic-scripts',
async () => {
const scripts = [
{ name: 'chinese-traditional', text: '發票編號:貳零貳肆' },
{ name: 'japanese-mixed', text: '請求書番号:2024年' },
{ name: 'korean', text: '송장 번호: 2024' },
{ name: 'thai', text: 'ใบแจ้งหนี้: ๒๐๒๔' },
{ name: 'devanagari', text: 'चालान संख्या: २०२४' },
{ name: 'cyrillic', text: 'Счёт-фактура № 2024' },
{ name: 'greek', text: 'Τιμολόγιο: ΜΜΚΔ' },
{ name: 'ethiopic', text: 'ቁጥር: ፪፻፳፬' },
{ name: 'bengali', text: 'চালান নং: ২০২৪' },
{ name: 'tamil', text: 'விலைப்பட்டியல்: ௨௦௨௪' }
];
const results = [];
for (const script of scripts) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>SCRIPT-${script.name}</ID>
<Description>${script.text}</Description>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const description = parsed?.Description || '';
results.push({
script: script.name,
success: true,
preserved: description === script.text,
charCount: script.text.length,
byteCount: Buffer.from(script.text, 'utf8').length
});
} catch (error) {
results.push({
script: script.name,
success: false,
error: error.message
});
}
}
return results;
}
);
exoticScripts.forEach(result => {
t.ok(result.success, `Script ${result.script} should be handled`);
if (result.success) {
t.ok(result.preserved, `Script ${result.script} content should be preserved`);
}
});
// Test 5: Invalid UTF-8 sequences
const invalidUTF8 = await performanceTracker.measureAsync(
'invalid-utf8-sequences',
async () => {
const invalidSequences = [
{
name: 'orphan-continuation',
bytes: Buffer.from([0x80, 0x81, 0x82])
},
{
name: 'incomplete-sequence',
bytes: Buffer.from([0xC2])
},
{
name: 'overlong-encoding',
bytes: Buffer.from([0xC0, 0x80])
},
{
name: 'invalid-start',
bytes: Buffer.from([0xF8, 0x80, 0x80, 0x80])
},
{
name: 'mixed-valid-invalid',
bytes: Buffer.concat([
Buffer.from('Valid '),
Buffer.from([0xFF, 0xFE]),
Buffer.from(' Text')
])
}
];
const results = [];
for (const seq of invalidSequences) {
const xmlStart = Buffer.from('<?xml version="1.0" encoding="UTF-8"?><Invoice><ID>');
const xmlEnd = Buffer.from('</ID></Invoice>');
const fullBuffer = Buffer.concat([xmlStart, seq.bytes, xmlEnd]);
try {
const parsed = await einvoice.parseDocument(fullBuffer);
results.push({
name: seq.name,
handled: true,
recovered: !!parsed,
replacedWithPlaceholder: true
});
} catch (error) {
results.push({
name: seq.name,
handled: true,
rejected: true,
error: error.message
});
}
}
return results;
}
);
invalidUTF8.forEach(result => {
t.ok(result.handled, `Invalid UTF-8 ${result.name} was handled`);
});
// Test 6: Normalization forms
const normalizationForms = await performanceTracker.measureAsync(
'unicode-normalization-forms',
async () => {
const testText = 'Café'; // Can be represented differently
const forms = [
{ name: 'NFC', text: testText.normalize('NFC') },
{ name: 'NFD', text: testText.normalize('NFD') },
{ name: 'NFKC', text: testText.normalize('NFKC') },
{ name: 'NFKD', text: testText.normalize('NFKD') }
];
const results = [];
for (const form of forms) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<CustomerName>${form.text}</CustomerName>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const name = parsed?.CustomerName || '';
results.push({
form: form.name,
success: true,
preserved: name === form.text,
normalized: name.normalize('NFC') === testText.normalize('NFC'),
codePoints: Array.from(form.text).length,
bytes: Buffer.from(form.text, 'utf8').length
});
} catch (error) {
results.push({
form: form.name,
success: false,
error: error.message
});
}
}
return results;
}
);
normalizationForms.forEach(result => {
t.ok(result.success, `Normalization form ${result.form} should be handled`);
if (result.success) {
t.ok(result.normalized, `Content should be comparable after normalization`);
}
});
// Test 7: Homoglyphs and confusables
const homoglyphsAndConfusables = await performanceTracker.measureAsync(
'homoglyphs-and-confusables',
async () => {
const confusables = [
{
name: 'latin-cyrillic-mix',
text: 'Invоicе Numbеr', // Contains Cyrillic о and е
description: 'Mixed Latin and Cyrillic lookalikes'
},
{
name: 'greek-latin-mix',
text: 'Ιnvoice Νumber', // Greek Ι and Ν
description: 'Greek letters that look like Latin'
},
{
name: 'fullwidth-chars',
text: ' ',
description: 'Fullwidth characters'
},
{
name: 'mathematical-alphanumeric',
text: '𝐈𝐧𝐯𝐨𝐢𝐜𝐞 𝐍𝐮𝐦𝐛𝐞𝐫',
description: 'Mathematical bold characters'
}
];
const results = [];
for (const test of confusables) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>${test.text}</ID>
<Note>${test.description}</Note>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const id = parsed?.ID || '';
// Check if system detects potential homoglyphs
const hasNonASCII = /[^\x00-\x7F]/.test(id);
const normalized = id.normalize('NFKC');
results.push({
name: test.name,
success: true,
preserved: id === test.text,
hasNonASCII,
normalized: normalized !== test.text,
detectable: hasNonASCII || normalized !== test.text
});
} catch (error) {
results.push({
name: test.name,
success: false,
error: error.message
});
}
}
return results;
}
);
homoglyphsAndConfusables.forEach(result => {
t.ok(result.success, `Homoglyph test ${result.name} should be handled`);
if (result.success) {
t.ok(result.detectable, `Potential confusables should be detectable`);
}
});
// Test 8: XML special characters in unusual encodings
const xmlSpecialInEncodings = await performanceTracker.measureAsync(
'xml-special-characters-in-encodings',
async () => {
const specialChars = [
{ char: '<', entity: '&lt;', desc: 'less than' },
{ char: '>', entity: '&gt;', desc: 'greater than' },
{ char: '&', entity: '&amp;', desc: 'ampersand' },
{ char: '"', entity: '&quot;', desc: 'quote' },
{ char: "'", entity: '&apos;', desc: 'apostrophe' }
];
const results = [];
for (const special of specialChars) {
// Test both raw and entity forms
const tests = [
{ type: 'entity', value: special.entity },
{ type: 'cdata', value: `<![CDATA[${special.char}]]>` },
{ type: 'numeric', value: `&#${special.char.charCodeAt(0)};` }
];
for (const test of tests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<Description>Price ${test.value} 100</Description>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const desc = parsed?.Description || '';
results.push({
char: special.desc,
method: test.type,
success: true,
containsChar: desc.includes(special.char),
preserved: true
});
} catch (error) {
results.push({
char: special.desc,
method: test.type,
success: false,
error: error.message
});
}
}
}
return results;
}
);
xmlSpecialInEncodings.forEach(result => {
t.ok(result.success, `XML special ${result.char} as ${result.method} should be handled`);
});
// Test 9: Private use area characters
const privateUseArea = await performanceTracker.measureAsync(
'private-use-area-characters',
async () => {
const puaRanges = [
{ name: 'BMP-PUA', start: 0xE000, end: 0xF8FF },
{ name: 'Plane15-PUA', start: 0xF0000, end: 0xFFFFD },
{ name: 'Plane16-PUA', start: 0x100000, end: 0x10FFFD }
];
const results = [];
for (const range of puaRanges) {
// Test a few characters from each range
const testChars = [];
testChars.push(String.fromCodePoint(range.start));
testChars.push(String.fromCodePoint(Math.floor((range.start + range.end) / 2)));
if (range.end <= 0x10FFFF) {
testChars.push(String.fromCodePoint(range.end));
}
const testString = testChars.join('');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<CustomField>${testString}</CustomField>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const field = parsed?.CustomField || '';
results.push({
range: range.name,
success: true,
preserved: field === testString,
charCount: testString.length,
handled: true
});
} catch (error) {
results.push({
range: range.name,
success: false,
error: error.message
});
}
}
return results;
}
);
privateUseArea.forEach(result => {
t.ok(result.success || result.error, `PUA range ${result.range} was processed`);
});
// Test 10: Character set conversion in format transformation
const formatTransformCharsets = await performanceTracker.measureAsync(
'format-transform-charsets',
async () => {
const testContents = [
{ name: 'multilingual', text: 'Hello مرحبا 你好 Здравствуйте' },
{ name: 'symbols', text: '€ £ ¥ $ ₹ ₽ ¢ ₩' },
{ name: 'accented', text: 'àáäâ èéëê ìíïî òóöô ùúüû ñç' },
{ name: 'mixed-emoji', text: 'Invoice 📄 Total: 💰 Status: ✅' }
];
const results = [];
for (const content of testContents) {
const ublInvoice = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CHARSET-001</ID>
<Note>${content.text}</Note>
</Invoice>`;
try {
// Convert to CII
const ciiResult = await einvoice.convertFormat(ublInvoice, 'cii');
// Parse the converted result
const parsed = await einvoice.parseDocument(ciiResult);
// Check if content was preserved
const preserved = JSON.stringify(parsed).includes(content.text);
results.push({
content: content.name,
success: true,
preserved,
formatConversionOk: true
});
} catch (error) {
results.push({
content: content.name,
success: false,
error: error.message
});
}
}
return results;
}
);
formatTransformCharsets.forEach(result => {
t.ok(result.success, `Format transform with ${result.content} should succeed`);
if (result.success) {
t.ok(result.preserved, `Character content should be preserved in transformation`);
}
});
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();

View File

@ -0,0 +1,526 @@
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-05: Zero-Byte PDFs');
tap.test('EDGE-05: Zero-Byte PDFs - should handle zero-byte and minimal PDF files', async (t) => {
const einvoice = new EInvoice();
// Test 1: Truly zero-byte PDF
const zeroByteFile = await performanceTracker.measureAsync(
'truly-zero-byte-pdf',
async () => {
const zeroPDF = Buffer.alloc(0);
try {
const result = await einvoice.extractFromPDF(zeroPDF);
return {
handled: true,
hasContent: !!result,
hasXML: result?.xml !== undefined,
hasAttachments: result?.attachments?.length > 0,
error: null,
bufferSize: zeroPDF.length
};
} catch (error) {
return {
handled: true,
hasContent: false,
error: error.message,
errorType: error.constructor.name,
bufferSize: zeroPDF.length
};
}
}
);
t.ok(zeroByteFile.handled, 'Zero-byte PDF was handled');
t.notOk(zeroByteFile.hasContent, 'Zero-byte PDF has no content');
t.equal(zeroByteFile.bufferSize, 0, 'Buffer size is zero');
// Test 2: Minimal PDF structure
const minimalPDFStructure = await performanceTracker.measureAsync(
'minimal-pdf-structure',
async () => {
const minimalPDFs = [
{
name: 'header-only',
content: Buffer.from('%PDF-1.4')
},
{
name: 'header-and-eof',
content: Buffer.from('%PDF-1.4\n%%EOF')
},
{
name: 'empty-catalog',
content: Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog >>\nendobj\n' +
'xref\n0 2\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'trailer\n<< /Size 2 /Root 1 0 R >>\n' +
'startxref\n64\n%%EOF'
)
},
{
name: 'single-empty-page',
content: Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n' +
'xref\n0 3\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000052 00000 n\n' +
'trailer\n<< /Size 3 /Root 1 0 R >>\n' +
'startxref\n110\n%%EOF'
)
}
];
const results = [];
for (const pdf of minimalPDFs) {
try {
const result = await einvoice.extractFromPDF(pdf.content);
results.push({
name: pdf.name,
size: pdf.content.length,
processed: true,
hasXML: !!result?.xml,
hasAttachments: result?.attachments?.length > 0,
hasMetadata: !!result?.metadata
});
} catch (error) {
results.push({
name: pdf.name,
size: pdf.content.length,
processed: false,
error: error.message
});
}
}
return results;
}
);
minimalPDFStructure.forEach(result => {
t.ok(result.processed || result.error, `Minimal PDF ${result.name} was processed`);
t.notOk(result.hasXML, `Minimal PDF ${result.name} has no XML`);
});
// Test 3: Truncated PDF files
const truncatedPDFs = await performanceTracker.measureAsync(
'truncated-pdf-files',
async () => {
// Start with a valid PDF structure and truncate at different points
const fullPDF = Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /Type /Pages /Count 1 /Kids [3 0 R] >>\nendobj\n' +
'3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n' +
'xref\n0 4\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000052 00000 n\n' +
'0000000110 00000 n\n' +
'trailer\n<< /Size 4 /Root 1 0 R >>\n' +
'startxref\n196\n%%EOF'
);
const truncationPoints = [
{ name: 'after-header', bytes: 10 },
{ name: 'mid-object', bytes: 50 },
{ name: 'before-xref', bytes: 150 },
{ name: 'mid-xref', bytes: 250 },
{ name: 'before-eof', bytes: fullPDF.length - 5 }
];
const results = [];
for (const point of truncationPoints) {
const truncated = fullPDF.slice(0, point.bytes);
try {
const result = await einvoice.extractFromPDF(truncated);
results.push({
truncationPoint: point.name,
size: truncated.length,
recovered: true,
hasPartialData: !!result
});
} catch (error) {
results.push({
truncationPoint: point.name,
size: truncated.length,
recovered: false,
error: error.message,
isCorruptionError: error.message.includes('corrupt') || error.message.includes('truncated')
});
}
}
return results;
}
);
truncatedPDFs.forEach(result => {
t.ok(!result.recovered || result.isCorruptionError,
`Truncated PDF at ${result.truncationPoint} should fail or be detected as corrupt`);
});
// Test 4: PDF with zero-byte attachment
const zeroByteAttachment = await performanceTracker.measureAsync(
'pdf-with-zero-byte-attachment',
async () => {
// Create a PDF with an embedded file of zero bytes
const pdfWithEmptyAttachment = Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Names 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /EmbeddedFiles 3 0 R >>\nendobj\n' +
'3 0 obj\n<< /Names [(empty.xml) 4 0 R] >>\nendobj\n' +
'4 0 obj\n<< /Type /Filespec /F (empty.xml) /EF << /F 5 0 R >> >>\nendobj\n' +
'5 0 obj\n<< /Type /EmbeddedFile /Length 0 >>\nstream\n\nendstream\nendobj\n' +
'xref\n0 6\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000062 00000 n\n' +
'0000000103 00000 n\n' +
'0000000151 00000 n\n' +
'0000000229 00000 n\n' +
'trailer\n<< /Size 6 /Root 1 0 R >>\n' +
'startxref\n307\n%%EOF'
);
try {
const result = await einvoice.extractFromPDF(pdfWithEmptyAttachment);
return {
processed: true,
hasAttachments: result?.attachments?.length > 0,
attachmentCount: result?.attachments?.length || 0,
firstAttachmentSize: result?.attachments?.[0]?.size || 0,
firstAttachmentName: result?.attachments?.[0]?.name || null
};
} catch (error) {
return {
processed: false,
error: error.message
};
}
}
);
t.ok(zeroByteAttachment.processed, 'PDF with zero-byte attachment was processed');
if (zeroByteAttachment.hasAttachments) {
t.equal(zeroByteAttachment.firstAttachmentSize, 0, 'Attachment size is zero');
}
// Test 5: PDF with only metadata
const metadataOnlyPDF = await performanceTracker.measureAsync(
'pdf-with-only-metadata',
async () => {
const pdfWithMetadata = Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Metadata 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /Type /Metadata /Subtype /XML /Length 100 >>\n' +
'stream\n' +
'<?xml version="1.0"?><x:xmpmeta xmlns:x="adobe:ns:meta/"><rdf:RDF></rdf:RDF></x:xmpmeta>\n' +
'endstream\nendobj\n' +
'xref\n0 3\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000068 00000 n\n' +
'trailer\n<< /Size 3 /Root 1 0 R >>\n' +
'startxref\n259\n%%EOF'
);
try {
const result = await einvoice.extractFromPDF(pdfWithMetadata);
return {
processed: true,
hasMetadata: !!result?.metadata,
hasXML: !!result?.xml,
hasContent: !!result?.content,
isEmpty: !result?.xml && !result?.attachments?.length
};
} catch (error) {
return {
processed: false,
error: error.message
};
}
}
);
t.ok(metadataOnlyPDF.processed, 'PDF with only metadata was processed');
t.ok(metadataOnlyPDF.isEmpty, 'PDF with only metadata has no invoice content');
// Test 6: Compressed empty streams
const compressedEmptyStreams = await performanceTracker.measureAsync(
'compressed-empty-streams',
async () => {
const compressionMethods = [
{ name: 'flate', filter: '/FlateDecode' },
{ name: 'lzw', filter: '/LZWDecode' },
{ name: 'ascii85', filter: '/ASCII85Decode' },
{ name: 'asciihex', filter: '/ASCIIHexDecode' }
];
const results = [];
for (const method of compressionMethods) {
const pdf = Buffer.from(
'%PDF-1.4\n' +
`1 0 obj\n<< /Length 0 /Filter ${method.filter} >>\n` +
'stream\n\nendstream\nendobj\n' +
'xref\n0 2\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'trailer\n<< /Size 2 >>\n' +
'startxref\n100\n%%EOF'
);
try {
const result = await einvoice.processPDFStream(pdf);
results.push({
method: method.name,
handled: true,
decompressed: true
});
} catch (error) {
results.push({
method: method.name,
handled: true,
error: error.message
});
}
}
return results;
}
);
compressedEmptyStreams.forEach(result => {
t.ok(result.handled, `Empty ${result.method} stream was handled`);
});
// Test 7: Zero-page PDF
const zeroPagePDF = await performanceTracker.measureAsync(
'zero-page-pdf',
async () => {
const zeroPagesPDF = Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /Type /Pages /Count 0 /Kids [] >>\nendobj\n' +
'xref\n0 3\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000058 00000 n\n' +
'trailer\n<< /Size 3 /Root 1 0 R >>\n' +
'startxref\n115\n%%EOF'
);
try {
const result = await einvoice.extractFromPDF(zeroPagesPDF);
return {
processed: true,
pageCount: result?.pageCount || 0,
hasContent: !!result?.content,
canExtractXML: !!result?.xml
};
} catch (error) {
return {
processed: false,
error: error.message
};
}
}
);
t.ok(zeroPagePDF.processed || zeroPagePDF.error, 'Zero-page PDF was handled');
if (zeroPagePDF.processed) {
t.equal(zeroPagePDF.pageCount, 0, 'Page count is zero');
}
// Test 8: PDF with empty form fields
const emptyFormFields = await performanceTracker.measureAsync(
'pdf-with-empty-form-fields',
async () => {
const formPDF = Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /AcroForm 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /Fields [] >>\nendobj\n' +
'xref\n0 3\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000065 00000 n\n' +
'trailer\n<< /Size 3 /Root 1 0 R >>\n' +
'startxref\n100\n%%EOF'
);
try {
const result = await einvoice.extractFromPDF(formPDF);
return {
processed: true,
hasForm: !!result?.form,
formFieldCount: result?.form?.fields?.length || 0,
hasData: !!result?.data
};
} catch (error) {
return {
processed: false,
error: error.message
};
}
}
);
t.ok(emptyFormFields.processed, 'PDF with empty form fields was processed');
// Test 9: Recovery attempts on zero-byte files
const recoveryAttempts = await performanceTracker.measureAsync(
'recovery-attempts-zero-byte',
async () => {
const corruptScenarios = [
{
name: 'no-header',
content: Buffer.from('This is not a PDF')
},
{
name: 'binary-garbage',
content: Buffer.from([0xFF, 0xFE, 0xFD, 0xFC, 0x00, 0x01, 0x02, 0x03])
},
{
name: 'html-instead',
content: Buffer.from('<html><body>Not a PDF</body></html>')
},
{
name: 'partial-header',
content: Buffer.from('%PDF-')
},
{
name: 'wrong-version',
content: Buffer.from('%PDF-99.9\n%%EOF')
}
];
const results = [];
for (const scenario of corruptScenarios) {
try {
const result = await einvoice.extractFromPDF(scenario.content, {
attemptRecovery: true
});
results.push({
scenario: scenario.name,
recovered: !!result,
hasAnyData: !!result?.xml || !!result?.attachments?.length
});
} catch (error) {
results.push({
scenario: scenario.name,
recovered: false,
errorMessage: error.message,
recognized: error.message.includes('PDF') || error.message.includes('format')
});
}
}
return results;
}
);
recoveryAttempts.forEach(result => {
t.ok(!result.recovered, `Recovery should fail for ${result.scenario}`);
t.ok(result.recognized, `Error should recognize invalid PDF format`);
});
// Test 10: Batch processing with zero-byte PDFs
const batchWithZeroBytes = await performanceTracker.measureAsync(
'batch-processing-zero-byte',
async () => {
const batch = [
{ name: 'normal', content: createValidPDF() },
{ name: 'zero-byte', content: Buffer.alloc(0) },
{ name: 'normal2', content: createValidPDF() },
{ name: 'header-only', content: Buffer.from('%PDF-1.4') },
{ name: 'normal3', content: createValidPDF() }
];
const results = {
total: batch.length,
successful: 0,
failed: 0,
skipped: 0,
errors: []
};
for (const item of batch) {
try {
const result = await einvoice.extractFromPDF(item.content);
if (result?.xml || result?.attachments?.length) {
results.successful++;
} else {
results.skipped++;
}
} catch (error) {
results.failed++;
results.errors.push({
name: item.name,
error: error.message
});
}
}
return results;
}
);
t.equal(batchWithZeroBytes.total,
batchWithZeroBytes.successful + batchWithZeroBytes.failed + batchWithZeroBytes.skipped,
'All batch items were processed');
t.ok(batchWithZeroBytes.failed > 0, 'Some zero-byte PDFs failed as expected');
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to create a valid PDF with invoice attachment
function createValidPDF(): Buffer {
return Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Names 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /EmbeddedFiles 3 0 R >>\nendobj\n' +
'3 0 obj\n<< /Names [(invoice.xml) 4 0 R] >>\nendobj\n' +
'4 0 obj\n<< /Type /Filespec /F (invoice.xml) /EF << /F 5 0 R >> >>\nendobj\n' +
'5 0 obj\n<< /Type /EmbeddedFile /Length 50 >>\nstream\n' +
'<?xml version="1.0"?><Invoice><ID>TEST</ID></Invoice>\n' +
'endstream\nendobj\n' +
'xref\n0 6\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000062 00000 n\n' +
'0000000103 00000 n\n' +
'0000000151 00000 n\n' +
'0000000229 00000 n\n' +
'trailer\n<< /Size 6 /Root 1 0 R >>\n' +
'startxref\n350\n%%EOF'
);
}
// Run the test
tap.start();

View File

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

View File

@ -0,0 +1,729 @@
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-07: Maximum Field Lengths');
tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allowed lengths', async (t) => {
const einvoice = new EInvoice();
// Test 1: Standard field length limits
const standardFieldLimits = await performanceTracker.measureAsync(
'standard-field-limits',
async () => {
const fieldTests = [
{ field: 'InvoiceID', maxLength: 200, standard: 'EN16931' },
{ field: 'CustomerName', maxLength: 200, standard: 'EN16931' },
{ field: 'Description', maxLength: 1000, standard: 'EN16931' },
{ field: 'Note', maxLength: 5000, standard: 'EN16931' },
{ field: 'Reference', maxLength: 200, standard: 'EN16931' },
{ field: 'Email', maxLength: 254, standard: 'RFC5321' },
{ field: 'Phone', maxLength: 30, standard: 'ITU-T' },
{ field: 'PostalCode', maxLength: 20, standard: 'UPU' }
];
const results = [];
for (const test of fieldTests) {
// Test at max length
const maxValue = 'X'.repeat(test.maxLength);
const xml = createInvoiceWithField(test.field, maxValue);
try {
const parsed = await einvoice.parseXML(xml);
const validated = await einvoice.validate(parsed);
results.push({
field: test.field,
length: test.maxLength,
parsed: true,
valid: validated?.isValid || false,
preserved: getFieldValue(parsed, test.field)?.length === test.maxLength
});
} catch (error) {
results.push({
field: test.field,
length: test.maxLength,
parsed: false,
error: error.message
});
}
// Test over max length
const overValue = 'X'.repeat(test.maxLength + 1);
const overXml = createInvoiceWithField(test.field, overValue);
try {
const parsed = await einvoice.parseXML(overXml);
const validated = await einvoice.validate(parsed);
results.push({
field: test.field,
length: test.maxLength + 1,
parsed: true,
valid: validated?.isValid || false,
truncated: getFieldValue(parsed, test.field)?.length <= test.maxLength
});
} catch (error) {
results.push({
field: test.field,
length: test.maxLength + 1,
parsed: false,
error: error.message
});
}
}
return results;
}
);
standardFieldLimits.forEach(result => {
if (result.length <= result.maxLength) {
t.ok(result.valid, `Field ${result.field} at max length should be valid`);
} else {
t.notOk(result.valid, `Field ${result.field} over max length should be invalid`);
}
});
// Test 2: Unicode character length vs byte length
const unicodeLengthTests = await performanceTracker.measureAsync(
'unicode-length-vs-bytes',
async () => {
const testCases = [
{
name: 'ascii-only',
char: 'A',
bytesPerChar: 1
},
{
name: 'latin-extended',
char: 'ñ',
bytesPerChar: 2
},
{
name: 'chinese',
char: '中',
bytesPerChar: 3
},
{
name: 'emoji',
char: '😀',
bytesPerChar: 4
}
];
const results = [];
const maxChars = 100;
for (const test of testCases) {
const value = test.char.repeat(maxChars);
const byteLength = Buffer.from(value, 'utf8').length;
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>TEST</ID>
<CustomerName>${value}</CustomerName>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const retrievedValue = parsed?.CustomerName || '';
results.push({
type: test.name,
charCount: value.length,
byteCount: byteLength,
expectedBytes: maxChars * test.bytesPerChar,
preserved: retrievedValue === value,
retrievedLength: retrievedValue.length,
retrievedBytes: Buffer.from(retrievedValue, 'utf8').length
});
} catch (error) {
results.push({
type: test.name,
charCount: value.length,
byteCount: byteLength,
error: error.message
});
}
}
return results;
}
);
unicodeLengthTests.forEach(result => {
t.ok(result.preserved || result.error,
`Unicode ${result.type} field should be handled correctly`);
if (result.preserved) {
t.equal(result.retrievedLength, result.charCount,
`Character count should be preserved for ${result.type}`);
}
});
// Test 3: Format-specific field limits
const formatSpecificLimits = await performanceTracker.measureAsync(
'format-specific-limits',
async () => {
const formatLimits = [
{
format: 'ubl',
fields: [
{ name: 'ID', maxLength: 200 },
{ name: 'Note', maxLength: 1000 },
{ name: 'DocumentCurrencyCode', maxLength: 3 }
]
},
{
format: 'cii',
fields: [
{ name: 'ID', maxLength: 35 },
{ name: 'Content', maxLength: 5000 },
{ name: 'TypeCode', maxLength: 4 }
]
},
{
format: 'xrechnung',
fields: [
{ name: 'BT-1', maxLength: 16 }, // Invoice number
{ name: 'BT-22', maxLength: 1000 }, // Note
{ name: 'BT-5', maxLength: 3 } // Currency
]
}
];
const results = [];
for (const format of formatLimits) {
for (const field of format.fields) {
const value = 'A'.repeat(field.maxLength);
const invoice = createFormatSpecificInvoice(format.format, field.name, value);
try {
const parsed = await einvoice.parseDocument(invoice);
const validated = await einvoice.validateFormat(parsed, format.format);
results.push({
format: format.format,
field: field.name,
maxLength: field.maxLength,
valid: validated?.isValid || false,
compliant: validated?.formatCompliant || false
});
} catch (error) {
results.push({
format: format.format,
field: field.name,
maxLength: field.maxLength,
error: error.message
});
}
}
}
return results;
}
);
formatSpecificLimits.forEach(result => {
t.ok(result.valid || result.error,
`${result.format} field ${result.field} at max length was processed`);
});
// Test 4: Extreme length edge cases
const extremeLengthCases = await performanceTracker.measureAsync(
'extreme-length-edge-cases',
async () => {
const extremeCases = [
{ length: 0, name: 'empty' },
{ length: 1, name: 'single-char' },
{ length: 255, name: 'common-db-limit' },
{ length: 65535, name: 'uint16-max' },
{ length: 1000000, name: 'one-million' },
{ length: 10000000, name: 'ten-million' }
];
const results = [];
for (const testCase of extremeCases) {
const value = testCase.length > 0 ? 'X'.repeat(testCase.length) : '';
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>EXTREME-${testCase.name}</ID>
<LongField>${value}</LongField>
</Invoice>`;
const startTime = Date.now();
const startMemory = process.memoryUsage();
try {
const parsed = await einvoice.parseXML(xml);
const endTime = Date.now();
const endMemory = process.memoryUsage();
results.push({
length: testCase.length,
name: testCase.name,
parsed: true,
timeTaken: endTime - startTime,
memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
fieldPreserved: parsed?.LongField?.length === testCase.length
});
} catch (error) {
results.push({
length: testCase.length,
name: testCase.name,
parsed: false,
error: error.message,
isLengthError: error.message.includes('length') || error.message.includes('size')
});
}
}
return results;
}
);
extremeLengthCases.forEach(result => {
if (result.length <= 65535) {
t.ok(result.parsed, `Length ${result.name} should be handled`);
} else {
t.ok(!result.parsed || result.isLengthError,
`Extreme length ${result.name} should be limited`);
}
});
// Test 5: Line item count limits
const lineItemCountLimits = await performanceTracker.measureAsync(
'line-item-count-limits',
async () => {
const itemCounts = [100, 1000, 9999, 10000, 99999];
const results = [];
for (const count of itemCounts) {
const invoice = createInvoiceWithManyItems(count);
const startTime = Date.now();
try {
const parsed = await einvoice.parseXML(invoice);
const itemsParsed = countItems(parsed);
const endTime = Date.now();
results.push({
requestedCount: count,
parsedCount: itemsParsed,
success: true,
timeTaken: endTime - startTime,
avgTimePerItem: (endTime - startTime) / count
});
} catch (error) {
results.push({
requestedCount: count,
success: false,
error: error.message
});
}
}
return results;
}
);
lineItemCountLimits.forEach(result => {
if (result.requestedCount <= 10000) {
t.ok(result.success, `${result.requestedCount} line items should be supported`);
}
});
// Test 6: Attachment size limits
const attachmentSizeLimits = await performanceTracker.measureAsync(
'attachment-size-limits',
async () => {
const sizes = [
{ size: 1024 * 1024, name: '1MB' },
{ size: 10 * 1024 * 1024, name: '10MB' },
{ size: 50 * 1024 * 1024, name: '50MB' },
{ size: 100 * 1024 * 1024, name: '100MB' }
];
const results = [];
for (const test of sizes) {
const attachmentData = Buffer.alloc(test.size, 'A');
const base64Data = attachmentData.toString('base64');
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>ATT-TEST</ID>
<Attachment>
<EmbeddedDocumentBinaryObject mimeCode="application/pdf">
${base64Data}
</EmbeddedDocumentBinaryObject>
</Attachment>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const attachment = extractAttachment(parsed);
results.push({
size: test.name,
bytes: test.size,
parsed: true,
attachmentPreserved: attachment?.length === test.size
});
} catch (error) {
results.push({
size: test.name,
bytes: test.size,
parsed: false,
error: error.message
});
}
}
return results;
}
);
attachmentSizeLimits.forEach(result => {
if (result.bytes <= 50 * 1024 * 1024) {
t.ok(result.parsed, `Attachment size ${result.size} should be supported`);
}
});
// Test 7: Decimal precision limits
const decimalPrecisionLimits = await performanceTracker.measureAsync(
'decimal-precision-limits',
async () => {
const precisionTests = [
{ decimals: 2, value: '12345678901234567890.12' },
{ decimals: 4, value: '123456789012345678.1234' },
{ decimals: 6, value: '1234567890123456.123456' },
{ decimals: 10, value: '123456789012.1234567890' },
{ decimals: 20, value: '12.12345678901234567890' },
{ decimals: 30, value: '1.123456789012345678901234567890' }
];
const results = [];
for (const test of precisionTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<TotalAmount currencyID="EUR">${test.value}</TotalAmount>
<Items>
<Item>
<Price>${test.value}</Price>
<Quantity>1</Quantity>
</Item>
</Items>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const amount = parsed?.TotalAmount;
// Check precision preservation
const preserved = amount?.toString() === test.value;
const rounded = amount?.toString() !== test.value;
results.push({
decimals: test.decimals,
originalValue: test.value,
parsedValue: amount?.toString(),
preserved,
rounded
});
} catch (error) {
results.push({
decimals: test.decimals,
error: error.message
});
}
}
return results;
}
);
decimalPrecisionLimits.forEach(result => {
t.ok(result.preserved || result.rounded,
`Decimal precision ${result.decimals} should be handled`);
});
// Test 8: Maximum nesting with field lengths
const nestingWithLengths = await performanceTracker.measureAsync(
'nesting-with-field-lengths',
async () => {
const createDeepStructure = (depth: number, fieldLength: number) => {
let xml = '';
const fieldValue = 'X'.repeat(fieldLength);
for (let i = 0; i < depth; i++) {
xml += `<Level${i}><Field${i}>${fieldValue}</Field${i}>`;
}
xml += '<Core>Data</Core>';
for (let i = depth - 1; i >= 0; i--) {
xml += `</Level${i}>`;
}
return xml;
};
const tests = [
{ depth: 10, fieldLength: 1000 },
{ depth: 50, fieldLength: 100 },
{ depth: 100, fieldLength: 10 },
{ depth: 5, fieldLength: 10000 }
];
const results = [];
for (const test of tests) {
const content = createDeepStructure(test.depth, test.fieldLength);
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>${content}</Invoice>`;
const totalDataSize = test.depth * test.fieldLength;
try {
const startTime = Date.now();
const parsed = await einvoice.parseXML(xml);
const endTime = Date.now();
results.push({
depth: test.depth,
fieldLength: test.fieldLength,
totalDataSize,
parsed: true,
timeTaken: endTime - startTime
});
} catch (error) {
results.push({
depth: test.depth,
fieldLength: test.fieldLength,
totalDataSize,
parsed: false,
error: error.message
});
}
}
return results;
}
);
nestingWithLengths.forEach(result => {
t.ok(result.parsed || result.error,
`Nested structure with depth ${result.depth} and field length ${result.fieldLength} was processed`);
});
// Test 9: Field truncation behavior
const fieldTruncationBehavior = await performanceTracker.measureAsync(
'field-truncation-behavior',
async () => {
const truncationTests = [
{
field: 'ID',
maxLength: 50,
testValue: 'A'.repeat(100),
truncationType: 'hard'
},
{
field: 'Note',
maxLength: 1000,
testValue: 'B'.repeat(2000),
truncationType: 'soft'
},
{
field: 'Email',
maxLength: 254,
testValue: 'x'.repeat(250) + '@test.com',
truncationType: 'smart'
}
];
const results = [];
for (const test of truncationTests) {
const xml = createInvoiceWithField(test.field, test.testValue);
try {
const parsed = await einvoice.parseXML(xml, {
truncateFields: true,
truncationMode: test.truncationType
});
const fieldValue = getFieldValue(parsed, test.field);
results.push({
field: test.field,
originalLength: test.testValue.length,
truncatedLength: fieldValue?.length || 0,
truncated: fieldValue?.length < test.testValue.length,
withinLimit: fieldValue?.length <= test.maxLength,
truncationType: test.truncationType
});
} catch (error) {
results.push({
field: test.field,
error: error.message
});
}
}
return results;
}
);
fieldTruncationBehavior.forEach(result => {
if (result.truncated) {
t.ok(result.withinLimit,
`Field ${result.field} should be truncated to within limit`);
}
});
// Test 10: Performance impact of field lengths
const performanceImpact = await performanceTracker.measureAsync(
'field-length-performance-impact',
async () => {
const lengths = [10, 100, 1000, 10000, 100000];
const results = [];
for (const length of lengths) {
const iterations = 10;
const times = [];
for (let i = 0; i < iterations; i++) {
const value = 'X'.repeat(length);
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>PERF-TEST</ID>
<Description>${value}</Description>
<Note>${value}</Note>
<CustomerName>${value}</CustomerName>
</Invoice>`;
const startTime = process.hrtime.bigint();
try {
await einvoice.parseXML(xml);
} catch (error) {
// Ignore errors for performance testing
}
const endTime = process.hrtime.bigint();
times.push(Number(endTime - startTime) / 1000000); // Convert to ms
}
const avgTime = times.reduce((a, b) => a + b, 0) / times.length;
results.push({
fieldLength: length,
avgParseTime: avgTime,
timePerKB: avgTime / (length * 3 / 1024) // 3 fields
});
}
return results;
}
);
// Verify performance doesn't degrade exponentially
const timeRatios = performanceImpact.map((r, i) =>
i > 0 ? r.avgParseTime / performanceImpact[i-1].avgParseTime : 1
);
timeRatios.forEach((ratio, i) => {
if (i > 0) {
t.ok(ratio < 15, `Performance scaling should be reasonable at length ${performanceImpact[i].fieldLength}`);
}
});
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to create invoice with specific field
function createInvoiceWithField(field: string, value: string): string {
const fieldMap = {
'InvoiceID': `<ID>${value}</ID>`,
'CustomerName': `<CustomerName>${value}</CustomerName>`,
'Description': `<Description>${value}</Description>`,
'Note': `<Note>${value}</Note>`,
'Reference': `<Reference>${value}</Reference>`,
'Email': `<Email>${value}</Email>`,
'Phone': `<Phone>${value}</Phone>`,
'PostalCode': `<PostalCode>${value}</PostalCode>`
};
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>TEST-001</ID>
${fieldMap[field] || `<${field}>${value}</${field}>`}
</Invoice>`;
}
// Helper function to get field value from parsed object
function getFieldValue(parsed: any, field: string): string | undefined {
return parsed?.[field] || parsed?.Invoice?.[field];
}
// Helper function to create format-specific invoice
function createFormatSpecificInvoice(format: string, field: string, value: string): string {
if (format === 'ubl') {
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<${field}>${value}</${field}>
</Invoice>`;
} else if (format === 'cii') {
return `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:${field}>${value}</rsm:${field}>
</rsm:CrossIndustryInvoice>`;
}
return createInvoiceWithField(field, value);
}
// Helper function to create invoice with many items
function createInvoiceWithManyItems(count: number): string {
let items = '';
for (let i = 0; i < count; i++) {
items += `<Item><ID>${i}</ID><Price>10.00</Price></Item>`;
}
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>MANY-ITEMS</ID>
<Items>${items}</Items>
</Invoice>`;
}
// Helper function to count items
function countItems(parsed: any): number {
if (!parsed?.Items) return 0;
if (Array.isArray(parsed.Items)) return parsed.Items.length;
if (parsed.Items.Item) {
return Array.isArray(parsed.Items.Item) ? parsed.Items.Item.length : 1;
}
return 0;
}
// Helper function to extract attachment
function extractAttachment(parsed: any): Buffer | null {
const base64Data = parsed?.Attachment?.EmbeddedDocumentBinaryObject;
if (base64Data) {
return Buffer.from(base64Data, 'base64');
}
return null;
}
// Run the test
tap.start();

View File

@ -0,0 +1,715 @@
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-08: Mixed Format Documents');
tap.test('EDGE-08: Mixed Format Documents - should handle documents with mixed or ambiguous formats', async (t) => {
const einvoice = new EInvoice();
// Test 1: Documents with elements from multiple standards
const multiStandardElements = await performanceTracker.measureAsync(
'multi-standard-elements',
async () => {
const mixedXML = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns:ubl="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cii="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
<!-- UBL elements -->
<ubl:ID>MIXED-001</ubl:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
<!-- CII elements -->
<cii:ExchangedDocument>
<ram:ID>MIXED-001-CII</ram:ID>
</cii:ExchangedDocument>
<!-- Custom elements -->
<CustomField>Custom Value</CustomField>
<!-- Mix of both -->
<LineItems>
<ubl:InvoiceLine>
<cbc:ID>1</cbc:ID>
</ubl:InvoiceLine>
<cii:SupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>2</ram:LineID>
</ram:AssociatedDocumentLineDocument>
</cii:SupplyChainTradeLineItem>
</LineItems>
</Invoice>`;
try {
const detection = await einvoice.detectFormat(mixedXML);
const parsed = await einvoice.parseDocument(mixedXML);
return {
detected: true,
primaryFormat: detection?.format,
confidence: detection?.confidence,
mixedElements: detection?.mixedElements || [],
standardsFound: detection?.detectedStandards || [],
parsed: !!parsed
};
} catch (error) {
return {
detected: false,
error: error.message
};
}
}
);
t.ok(multiStandardElements.detected, 'Multi-standard document was processed');
t.ok(multiStandardElements.standardsFound?.length > 1, 'Multiple standards detected');
// Test 2: Namespace confusion
const namespaceConfusion = await performanceTracker.measureAsync(
'namespace-confusion',
async () => {
const confusedNamespaces = [
{
name: 'wrong-namespace-binding',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<!-- Using CII elements in UBL namespace -->
<ExchangedDocument>
<ID>CONFUSED-001</ID>
</ExchangedDocument>
</Invoice>`
},
{
name: 'conflicting-default-namespaces',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<root>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>UBL-001</ID>
</Invoice>
<Invoice xmlns="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<ExchangedDocument>
<ID>CII-001</ID>
</ExchangedDocument>
</Invoice>
</root>`
},
{
name: 'namespace-switching',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>START-UBL</ID>
<Items xmlns="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<SupplyChainTradeLineItem>
<LineID xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">1</LineID>
</SupplyChainTradeLineItem>
</Items>
</Invoice>`
}
];
const results = [];
for (const test of confusedNamespaces) {
try {
const detection = await einvoice.detectFormat(test.xml);
const parsed = await einvoice.parseDocument(test.xml);
const validation = await einvoice.validate(parsed);
results.push({
scenario: test.name,
detected: true,
format: detection?.format,
hasNamespaceIssues: detection?.namespaceIssues || false,
valid: validation?.isValid || false,
warnings: validation?.warnings || []
});
} catch (error) {
results.push({
scenario: test.name,
detected: false,
error: error.message
});
}
}
return results;
}
);
namespaceConfusion.forEach(result => {
t.ok(result.detected || result.error, `Namespace confusion ${result.scenario} was handled`);
if (result.detected) {
t.ok(result.hasNamespaceIssues || result.warnings.length > 0,
'Namespace issues should be detected');
}
});
// Test 3: Hybrid PDF documents
const hybridPDFDocuments = await performanceTracker.measureAsync(
'hybrid-pdf-documents',
async () => {
const hybridScenarios = [
{
name: 'multiple-xml-attachments',
description: 'PDF with both UBL and CII XML attachments'
},
{
name: 'conflicting-metadata',
description: 'PDF metadata says ZUGFeRD but contains Factur-X'
},
{
name: 'mixed-version-attachments',
description: 'PDF with ZUGFeRD v1 and v2 attachments'
},
{
name: 'non-standard-attachment',
description: 'PDF with standard XML plus custom format'
}
];
const results = [];
for (const scenario of hybridScenarios) {
// Create mock hybrid PDF
const hybridPDF = createHybridPDF(scenario.name);
try {
const extraction = await einvoice.extractFromPDF(hybridPDF);
results.push({
scenario: scenario.name,
extracted: true,
attachmentCount: extraction?.attachments?.length || 0,
formats: extraction?.detectedFormats || [],
primaryFormat: extraction?.primaryFormat,
hasConflicts: extraction?.hasFormatConflicts || false
});
} catch (error) {
results.push({
scenario: scenario.name,
extracted: false,
error: error.message
});
}
}
return results;
}
);
hybridPDFDocuments.forEach(result => {
t.ok(result.extracted || result.error,
`Hybrid PDF ${result.scenario} was processed`);
});
// Test 4: Schema version mixing
const schemaVersionMixing = await performanceTracker.measureAsync(
'schema-version-mixing',
async () => {
const versionMixes = [
{
name: 'ubl-version-mix',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2"
xmlns:cbc1="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-1">
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
<cbc1:ID>OLD-STYLE-ID</cbc1:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
</Invoice>`
},
{
name: 'zugferd-version-mix',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryDocument>
<!-- ZUGFeRD 1.0 structure -->
<rsm:SpecifiedExchangedDocumentContext>
<ram:GuidelineSpecifiedDocumentContextParameter>
<ram:ID>urn:ferd:CrossIndustryDocument:invoice:1p0</ram:ID>
</ram:GuidelineSpecifiedDocumentContextParameter>
</rsm:SpecifiedExchangedDocumentContext>
<!-- ZUGFeRD 2.1 elements -->
<rsm:ExchangedDocument>
<ram:ID>MIXED-VERSION</ram:ID>
</rsm:ExchangedDocument>
</rsm:CrossIndustryDocument>`
}
];
const results = [];
for (const mix of versionMixes) {
try {
const detection = await einvoice.detectFormat(mix.xml);
const parsed = await einvoice.parseDocument(mix.xml);
results.push({
scenario: mix.name,
processed: true,
detectedVersion: detection?.version,
versionConflicts: detection?.versionConflicts || [],
canMigrate: detection?.migrationPath !== undefined
});
} catch (error) {
results.push({
scenario: mix.name,
processed: false,
error: error.message
});
}
}
return results;
}
);
schemaVersionMixing.forEach(result => {
t.ok(result.processed || result.error,
`Version mix ${result.scenario} was handled`);
});
// Test 5: Invalid format combinations
const invalidFormatCombos = await performanceTracker.measureAsync(
'invalid-format-combinations',
async () => {
const invalidCombos = [
{
name: 'ubl-with-cii-structure',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<!-- CII structure in UBL namespace -->
<rsm:ExchangedDocumentContext>
<ram:BusinessProcessSpecifiedDocumentContextParameter/>
</rsm:ExchangedDocumentContext>
<rsm:ExchangedDocument>
<ram:ID>INVALID-001</ram:ID>
</rsm:ExchangedDocument>
</Invoice>`
},
{
name: 'html-invoice-hybrid',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<html>
<body>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>HTML-WRAPPED</ID>
</Invoice>
</body>
</html>`
},
{
name: 'json-xml-mix',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>JSON-MIX</ID>
<JSONData>
{"amount": 100, "currency": "EUR"}
</JSONData>
<XMLData>
<Amount>100</Amount>
<Currency>EUR</Currency>
</XMLData>
</Invoice>`
}
];
const results = [];
for (const combo of invalidCombos) {
try {
const detection = await einvoice.detectFormat(combo.xml);
const parsed = await einvoice.parseDocument(combo.xml);
const validation = await einvoice.validate(parsed);
results.push({
combo: combo.name,
detected: !!detection,
format: detection?.format || 'unknown',
valid: validation?.isValid || false,
recoverable: detection?.canRecover || false
});
} catch (error) {
results.push({
combo: combo.name,
detected: false,
error: error.message
});
}
}
return results;
}
);
invalidFormatCombos.forEach(result => {
t.notOk(result.valid, `Invalid combo ${result.combo} should not validate`);
});
// Test 6: Partial format documents
const partialFormatDocuments = await performanceTracker.measureAsync(
'partial-format-documents',
async () => {
const partials = [
{
name: 'ubl-header-cii-body',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<cbc:ID>PARTIAL-001</cbc:ID>
<cbc:IssueDate>2024-01-15</cbc:IssueDate>
<!-- Switch to CII for line items -->
<IncludedSupplyChainTradeLineItem>
<ram:AssociatedDocumentLineDocument>
<ram:LineID>1</ram:LineID>
</ram:AssociatedDocumentLineDocument>
</IncludedSupplyChainTradeLineItem>
</Invoice>`
},
{
name: 'incomplete-migration',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<!-- Old format -->
<InvoiceNumber>OLD-001</InvoiceNumber>
<!-- New format -->
<ID>NEW-001</ID>
<!-- Mixed date formats -->
<InvoiceDate>2024-01-15</InvoiceDate>
<IssueDate>2024-01-15T00:00:00</IssueDate>
</Invoice>`
}
];
const results = [];
for (const partial of partials) {
try {
const analysis = await einvoice.analyzeDocument(partial.xml);
results.push({
scenario: partial.name,
analyzed: true,
completeness: analysis?.completeness || 0,
missingElements: analysis?.missingElements || [],
formatConsistency: analysis?.formatConsistency || 0,
migrationNeeded: analysis?.requiresMigration || false
});
} catch (error) {
results.push({
scenario: partial.name,
analyzed: false,
error: error.message
});
}
}
return results;
}
);
partialFormatDocuments.forEach(result => {
t.ok(result.analyzed || result.error,
`Partial document ${result.scenario} was analyzed`);
if (result.analyzed) {
t.ok(result.formatConsistency < 100,
'Format inconsistency should be detected');
}
});
// Test 7: Encoding format conflicts
const encodingFormatConflicts = await performanceTracker.measureAsync(
'encoding-format-conflicts',
async () => {
const encodingConflicts = [
{
name: 'utf8-with-utf16-content',
declared: 'UTF-8',
actual: 'UTF-16',
content: Buffer.from('<?xml version="1.0" encoding="UTF-8"?><Invoice><ID>TEST</ID></Invoice>', 'utf16le')
},
{
name: 'wrong-decimal-separator',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<!-- European format in US-style document -->
<TotalAmount>1.234,56</TotalAmount>
<TaxAmount>234.56</TaxAmount>
</Invoice>`
},
{
name: 'date-format-mixing',
xml: `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<Dates>
<IssueDate>2024-01-15</IssueDate>
<DueDate>15/01/2024</DueDate>
<DeliveryDate>01-15-2024</DeliveryDate>
<PaymentDate>20240115</PaymentDate>
</Dates>
</Invoice>`
}
];
const results = [];
for (const conflict of encodingConflicts) {
try {
let parseResult;
if (conflict.content) {
parseResult = await einvoice.parseDocument(conflict.content);
} else {
parseResult = await einvoice.parseDocument(conflict.xml);
}
const analysis = await einvoice.analyzeFormatConsistency(parseResult);
results.push({
scenario: conflict.name,
handled: true,
encodingIssues: analysis?.encodingIssues || [],
formatIssues: analysis?.formatIssues || [],
normalized: analysis?.normalized || false
});
} catch (error) {
results.push({
scenario: conflict.name,
handled: false,
error: error.message
});
}
}
return results;
}
);
encodingFormatConflicts.forEach(result => {
t.ok(result.handled || result.error,
`Encoding conflict ${result.scenario} was handled`);
});
// Test 8: Format autodetection challenges
const autodetectionChallenges = await performanceTracker.measureAsync(
'format-autodetection-challenges',
async () => {
const challenges = [
{
name: 'minimal-structure',
xml: '<Invoice><ID>123</ID></Invoice>'
},
{
name: 'generic-xml',
xml: `<?xml version="1.0"?>
<Document>
<Header>
<ID>DOC-001</ID>
<Date>2024-01-15</Date>
</Header>
<Items>
<Item>
<Description>Product</Description>
<Amount>100</Amount>
</Item>
</Items>
</Document>`
},
{
name: 'custom-namespace',
xml: `<?xml version="1.0"?>
<inv:Invoice xmlns:inv="http://custom.company.com/invoice">
<inv:Number>INV-001</inv:Number>
<inv:Total>1000</inv:Total>
</inv:Invoice>`
}
];
const results = [];
for (const challenge of challenges) {
const detectionResult = await einvoice.detectFormat(challenge.xml);
results.push({
scenario: challenge.name,
format: detectionResult?.format || 'unknown',
confidence: detectionResult?.confidence || 0,
isGeneric: detectionResult?.isGeneric || false,
suggestedFormats: detectionResult?.possibleFormats || []
});
}
return results;
}
);
autodetectionChallenges.forEach(result => {
t.ok(result.confidence < 100 || result.isGeneric,
`Challenge ${result.scenario} should have detection uncertainty`);
});
// Test 9: Legacy format mixing
const legacyFormatMixing = await performanceTracker.measureAsync(
'legacy-format-mixing',
async () => {
const legacyMixes = [
{
name: 'edifact-xml-hybrid',
content: `UNB+UNOC:3+SENDER+RECEIVER+240115:1200+1++INVOIC'
<?xml version="1.0"?>
<AdditionalData>
<Invoice>
<ID>HYBRID-001</ID>
</Invoice>
</AdditionalData>
UNZ+1+1'`
},
{
name: 'csv-xml-combination',
content: `INVOICE_HEADER
ID,Date,Amount
INV-001,2024-01-15,1000.00
<?xml version="1.0"?>
<InvoiceDetails>
<LineItems>
<Item>Product A</Item>
</LineItems>
</InvoiceDetails>`
}
];
const results = [];
for (const mix of legacyMixes) {
try {
const detection = await einvoice.detectFormat(mix.content);
const extraction = await einvoice.extractStructuredData(mix.content);
results.push({
scenario: mix.name,
processed: true,
formatsFound: detection?.multipleFormats || [],
primaryFormat: detection?.primaryFormat,
dataExtracted: !!extraction?.data
});
} catch (error) {
results.push({
scenario: mix.name,
processed: false,
error: error.message
});
}
}
return results;
}
);
legacyFormatMixing.forEach(result => {
t.ok(result.processed || result.error,
`Legacy mix ${result.scenario} was handled`);
});
// Test 10: Format conversion conflicts
const formatConversionConflicts = await performanceTracker.measureAsync(
'format-conversion-conflicts',
async () => {
// Create invoice with format-specific features
const sourceInvoice = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>CONVERT-001</ID>
<!-- UBL-specific extension -->
<UBLExtensions>
<UBLExtension>
<ExtensionContent>
<CustomField>UBL-Only-Data</CustomField>
</ExtensionContent>
</UBLExtension>
</UBLExtensions>
<!-- Format-specific calculation -->
<AllowanceCharge>
<ChargeIndicator>false</ChargeIndicator>
<AllowanceChargeReason>Discount</AllowanceChargeReason>
<Amount currencyID="EUR">50.00</Amount>
</AllowanceCharge>
</Invoice>`;
const targetFormats = ['cii', 'xrechnung', 'fatturapa'];
const results = [];
for (const target of targetFormats) {
try {
const converted = await einvoice.convertFormat(sourceInvoice, target);
const analysis = await einvoice.analyzeConversion(sourceInvoice, converted);
results.push({
targetFormat: target,
converted: true,
dataLoss: analysis?.dataLoss || [],
unsupportedFeatures: analysis?.unsupportedFeatures || [],
warnings: analysis?.warnings || []
});
} catch (error) {
results.push({
targetFormat: target,
converted: false,
error: error.message
});
}
}
return results;
}
);
formatConversionConflicts.forEach(result => {
t.ok(result.converted || result.error,
`Conversion to ${result.targetFormat} was attempted`);
if (result.converted) {
t.ok(result.dataLoss.length > 0 || result.warnings.length > 0,
'Format-specific features should cause warnings');
}
});
// Print performance summary
performanceTracker.printSummary();
});
// Helper function to create hybrid PDF
function createHybridPDF(scenario: string): Buffer {
// Simplified mock - in reality would create actual PDF structure
const mockStructure = {
'multiple-xml-attachments': {
attachments: [
{ name: 'invoice.ubl.xml', type: 'application/xml' },
{ name: 'invoice.cii.xml', type: 'application/xml' }
]
},
'conflicting-metadata': {
metadata: { format: 'ZUGFeRD' },
attachments: [{ name: 'facturx.xml', type: 'application/xml' }]
},
'mixed-version-attachments': {
attachments: [
{ name: 'zugferd_v1.xml', version: '1.0' },
{ name: 'zugferd_v2.xml', version: '2.1' }
]
},
'non-standard-attachment': {
attachments: [
{ name: 'invoice.xml', type: 'application/xml' },
{ name: 'custom.json', type: 'application/json' }
]
}
};
return Buffer.from(JSON.stringify(mockStructure[scenario] || {}));
}
// Run the test
tap.start();

View File

@ -0,0 +1,804 @@
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-09: Corrupted ZIP Containers');
tap.test('EDGE-09: Corrupted ZIP Containers - should handle corrupted ZIP/container files', async (t) => {
const einvoice = new EInvoice();
// Test 1: Invalid ZIP headers
const invalidZipHeaders = await performanceTracker.measureAsync(
'invalid-zip-headers',
async () => {
const corruptHeaders = [
{
name: 'wrong-magic-bytes',
data: Buffer.from('NOTAZIP\x00\x00\x00\x00'),
description: 'Invalid ZIP signature'
},
{
name: 'partial-header',
data: Buffer.from('PK\x03'),
description: 'Incomplete ZIP header'
},
{
name: 'corrupted-local-header',
data: Buffer.concat([
Buffer.from('PK\x03\x04'), // Local file header signature
Buffer.from([0xFF, 0xFF, 0xFF, 0xFF]), // Corrupted version/flags
Buffer.alloc(20, 0) // Rest of header
]),
description: 'Corrupted local file header'
},
{
name: 'missing-central-directory',
data: Buffer.concat([
Buffer.from('PK\x03\x04'), // Local file header
Buffer.alloc(26, 0), // Header data
Buffer.from('PK\x07\x08'), // Data descriptor
Buffer.alloc(12, 0), // Descriptor data
// Missing central directory
]),
description: 'Missing central directory'
}
];
const results = [];
for (const corrupt of corruptHeaders) {
try {
const extracted = await einvoice.extractFromContainer(corrupt.data);
results.push({
type: corrupt.name,
recovered: !!extracted,
filesExtracted: extracted?.files?.length || 0,
error: null
});
} catch (error) {
results.push({
type: corrupt.name,
recovered: false,
error: error.message,
isZipError: error.message.toLowerCase().includes('zip') ||
error.message.toLowerCase().includes('archive')
});
}
}
return results;
}
);
invalidZipHeaders.forEach(result => {
t.ok(!result.recovered || result.isZipError,
`Invalid header ${result.type} should fail or be detected`);
});
// Test 2: Truncated ZIP files
const truncatedZipFiles = await performanceTracker.measureAsync(
'truncated-zip-files',
async () => {
// Create a valid ZIP structure and truncate at different points
const validZip = createValidZipStructure();
const truncationPoints = [
{ point: 10, name: 'header-truncated' },
{ point: 50, name: 'file-data-truncated' },
{ point: validZip.length - 50, name: 'directory-truncated' },
{ point: validZip.length - 10, name: 'eocd-truncated' },
{ point: validZip.length - 1, name: 'last-byte-missing' }
];
const results = [];
for (const truncation of truncationPoints) {
const truncated = validZip.slice(0, truncation.point);
try {
const recovery = await einvoice.recoverFromCorruptedZip(truncated, {
attemptPartialRecovery: true
});
results.push({
truncation: truncation.name,
size: truncated.length,
recovered: recovery?.success || false,
filesRecovered: recovery?.recoveredFiles || 0,
dataRecovered: recovery?.recoveredBytes || 0
});
} catch (error) {
results.push({
truncation: truncation.name,
size: truncated.length,
recovered: false,
error: error.message
});
}
}
return results;
}
);
truncatedZipFiles.forEach(result => {
t.ok(result.recovered === false || result.filesRecovered < 1,
`Truncated ZIP at ${result.truncation} should have limited recovery`);
});
// Test 3: CRC errors
const crcErrors = await performanceTracker.measureAsync(
'crc-checksum-errors',
async () => {
const scenarios = [
{
name: 'single-bit-flip',
corruption: (data: Buffer) => {
const copy = Buffer.from(data);
// Flip a bit in the compressed data
if (copy.length > 100) {
copy[100] ^= 0x01;
}
return copy;
}
},
{
name: 'data-corruption',
corruption: (data: Buffer) => {
const copy = Buffer.from(data);
// Corrupt a chunk of data
for (let i = 50; i < Math.min(100, copy.length); i++) {
copy[i] = 0xFF;
}
return copy;
}
},
{
name: 'wrong-crc-stored',
corruption: (data: Buffer) => {
const copy = Buffer.from(data);
// Find and corrupt CRC values
const crcOffset = findCRCOffset(copy);
if (crcOffset > 0) {
copy.writeUInt32LE(0xDEADBEEF, crcOffset);
}
return copy;
}
}
];
const results = [];
for (const scenario of scenarios) {
const validZip = createZipWithInvoice();
const corrupted = scenario.corruption(validZip);
try {
const extraction = await einvoice.extractFromContainer(corrupted, {
ignoreCRCErrors: false
});
results.push({
scenario: scenario.name,
extracted: true,
crcValidated: extraction?.crcValid || false,
dataIntegrity: extraction?.integrityCheck || 'unknown'
});
} catch (error) {
results.push({
scenario: scenario.name,
extracted: false,
error: error.message,
isCRCError: error.message.toLowerCase().includes('crc') ||
error.message.toLowerCase().includes('checksum')
});
}
}
return results;
}
);
crcErrors.forEach(result => {
t.ok(!result.extracted || !result.crcValidated || result.isCRCError,
`CRC error ${result.scenario} should be detected`);
});
// Test 4: Compression method issues
const compressionMethodIssues = await performanceTracker.measureAsync(
'compression-method-issues',
async () => {
const compressionTests = [
{
name: 'unsupported-method',
method: 99, // Invalid compression method
description: 'Unknown compression algorithm'
},
{
name: 'store-but-compressed',
method: 0, // Store (no compression)
compressed: true,
description: 'Stored method but data is compressed'
},
{
name: 'deflate-corrupted',
method: 8, // Deflate
corrupted: true,
description: 'Deflate stream corrupted'
},
{
name: 'bzip2-in-zip',
method: 12, // Bzip2 (not standard in ZIP)
description: 'Non-standard compression method'
}
];
const results = [];
for (const test of compressionTests) {
const zipData = createZipWithCompressionMethod(test.method, test);
try {
const extracted = await einvoice.extractFromContainer(zipData);
results.push({
test: test.name,
method: test.method,
extracted: true,
filesFound: extracted?.files?.length || 0,
decompressed: extracted?.decompressed || false
});
} catch (error) {
results.push({
test: test.name,
method: test.method,
extracted: false,
error: error.message,
isCompressionError: error.message.includes('compress') ||
error.message.includes('method')
});
}
}
return results;
}
);
compressionMethodIssues.forEach(result => {
if (result.method === 0 || result.method === 8) {
t.ok(result.extracted || result.isCompressionError,
`Standard compression ${result.test} should be handled`);
} else {
t.notOk(result.extracted,
`Non-standard compression ${result.test} should fail`);
}
});
// Test 5: Nested/recursive ZIP bombs
const nestedZipBombs = await performanceTracker.measureAsync(
'nested-zip-bombs',
async () => {
const bombTypes = [
{
name: 'deep-nesting',
depth: 10,
description: 'ZIP within ZIP, 10 levels deep'
},
{
name: 'exponential-expansion',
copies: 10,
description: 'Each level contains 10 copies'
},
{
name: 'circular-reference',
circular: true,
description: 'ZIP contains itself'
},
{
name: 'compression-ratio-bomb',
ratio: 1000,
description: 'Extreme compression ratio'
}
];
const results = [];
for (const bomb of bombTypes) {
const bombZip = createZipBomb(bomb);
const startTime = Date.now();
const startMemory = process.memoryUsage();
try {
const extraction = await einvoice.extractFromContainer(bombZip, {
maxDepth: 5,
maxExpandedSize: 100 * 1024 * 1024, // 100MB limit
maxFiles: 1000
});
const endTime = Date.now();
const endMemory = process.memoryUsage();
results.push({
type: bomb.name,
handled: true,
timeTaken: endTime - startTime,
memoryUsed: endMemory.heapUsed - startMemory.heapUsed,
depthReached: extraction?.maxDepth || 0,
stopped: extraction?.limitReached || false
});
} catch (error) {
results.push({
type: bomb.name,
handled: true,
prevented: true,
error: error.message,
isBombDetected: error.message.includes('bomb') ||
error.message.includes('depth') ||
error.message.includes('limit')
});
}
}
return results;
}
);
nestedZipBombs.forEach(result => {
t.ok(result.prevented || result.stopped,
`ZIP bomb ${result.type} should be prevented or limited`);
});
// Test 6: Character encoding in filenames
const filenameEncodingIssues = await performanceTracker.measureAsync(
'filename-encoding-issues',
async () => {
const encodingTests = [
{
name: 'utf8-bom-filename',
filename: '\uFEFFファイル.xml',
encoding: 'utf8'
},
{
name: 'cp437-extended',
filename: 'Ñoño_español.xml',
encoding: 'cp437'
},
{
name: 'mixed-encoding',
filename: 'Test_文件_файл.xml',
encoding: 'mixed'
},
{
name: 'null-bytes',
filename: 'file\x00.xml',
encoding: 'binary'
},
{
name: 'path-traversal',
filename: '../../../etc/passwd',
encoding: 'ascii'
}
];
const results = [];
for (const test of encodingTests) {
const zipData = createZipWithFilename(test.filename, test.encoding);
try {
const extracted = await einvoice.extractFromContainer(zipData);
const files = extracted?.files || [];
results.push({
test: test.name,
extracted: true,
fileCount: files.length,
filenamePreserved: files.some(f => f.name === test.filename),
filenameNormalized: files[0]?.name || null,
securityCheck: !files.some(f => f.name.includes('..'))
});
} catch (error) {
results.push({
test: test.name,
extracted: false,
error: error.message
});
}
}
return results;
}
);
filenameEncodingIssues.forEach(result => {
t.ok(result.securityCheck,
`Filename ${result.test} should pass security checks`);
});
// Test 7: Factur-X/ZUGFeRD specific corruptions
const facturXCorruptions = await performanceTracker.measureAsync(
'facturx-zugferd-corruptions',
async () => {
const corruptionTypes = [
{
name: 'missing-metadata',
description: 'PDF/A-3 without required metadata'
},
{
name: 'wrong-attachment-relationship',
description: 'XML not marked as Alternative'
},
{
name: 'multiple-xml-versions',
description: 'Both Factur-X and ZUGFeRD XML present'
},
{
name: 'corrupted-xml-stream',
description: 'XML attachment stream corrupted'
}
];
const results = [];
for (const corruption of corruptionTypes) {
const corruptedPDF = createCorruptedFacturX(corruption.name);
try {
const extraction = await einvoice.extractFromPDF(corruptedPDF);
results.push({
corruption: corruption.name,
extracted: !!extraction,
hasValidXML: extraction?.xml && isValidXML(extraction.xml),
hasMetadata: !!extraction?.metadata,
conformance: extraction?.conformanceLevel || 'unknown'
});
} catch (error) {
results.push({
corruption: corruption.name,
extracted: false,
error: error.message
});
}
}
return results;
}
);
facturXCorruptions.forEach(result => {
t.ok(result.extracted || result.error,
`Factur-X corruption ${result.corruption} was handled`);
});
// Test 8: Recovery strategies
const recoveryStrategies = await performanceTracker.measureAsync(
'zip-recovery-strategies',
async () => {
const strategies = [
{
name: 'scan-for-headers',
description: 'Scan for local file headers'
},
{
name: 'reconstruct-central-dir',
description: 'Rebuild central directory'
},
{
name: 'raw-deflate-extraction',
description: 'Extract raw deflate streams'
},
{
name: 'pattern-matching',
description: 'Find XML by pattern matching'
}
];
const corruptedZip = createSeverelyCorruptedZip();
const results = [];
for (const strategy of strategies) {
try {
const recovered = await einvoice.attemptZipRecovery(corruptedZip, {
strategy: strategy.name
});
results.push({
strategy: strategy.name,
success: recovered?.success || false,
filesRecovered: recovered?.files?.length || 0,
xmlFound: recovered?.files?.some(f => f.name.endsWith('.xml')) || false,
confidence: recovered?.confidence || 0
});
} catch (error) {
results.push({
strategy: strategy.name,
success: false,
error: error.message
});
}
}
return results;
}
);
recoveryStrategies.forEach(result => {
t.ok(result.success || result.error,
`Recovery strategy ${result.strategy} was attempted`);
});
// Test 9: Multi-part archive issues
const multiPartArchiveIssues = await performanceTracker.measureAsync(
'multi-part-archive-issues',
async () => {
const multiPartTests = [
{
name: 'missing-parts',
parts: ['part1.zip', null, 'part3.zip'],
description: 'Missing middle part'
},
{
name: 'wrong-order',
parts: ['part3.zip', 'part1.zip', 'part2.zip'],
description: 'Parts in wrong order'
},
{
name: 'mixed-formats',
parts: ['part1.zip', 'part2.rar', 'part3.zip'],
description: 'Different archive formats'
},
{
name: 'size-mismatch',
parts: createMismatchedParts(),
description: 'Part sizes do not match'
}
];
const results = [];
for (const test of multiPartTests) {
try {
const assembled = await einvoice.assembleMultiPartArchive(test.parts);
const extracted = await einvoice.extractFromContainer(assembled);
results.push({
test: test.name,
assembled: true,
extracted: !!extracted,
complete: extracted?.isComplete || false
});
} catch (error) {
results.push({
test: test.name,
assembled: false,
error: error.message
});
}
}
return results;
}
);
multiPartArchiveIssues.forEach(result => {
t.ok(!result.assembled || !result.complete,
`Multi-part issue ${result.test} should cause problems`);
});
// Test 10: Performance with corrupted files
const corruptedPerformance = await performanceTracker.measureAsync(
'corrupted-file-performance',
async () => {
const sizes = [
{ size: 1024, name: '1KB' },
{ size: 1024 * 1024, name: '1MB' },
{ size: 10 * 1024 * 1024, name: '10MB' }
];
const results = [];
for (const sizeTest of sizes) {
// Create corrupted file of specific size
const corrupted = createCorruptedZipOfSize(sizeTest.size);
const startTime = Date.now();
const timeout = 10000; // 10 second timeout
try {
const extractPromise = einvoice.extractFromContainer(corrupted);
const timeoutPromise = new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeout)
);
await Promise.race([extractPromise, timeoutPromise]);
const timeTaken = Date.now() - startTime;
results.push({
size: sizeTest.name,
completed: true,
timeTaken,
timedOut: false
});
} catch (error) {
const timeTaken = Date.now() - startTime;
results.push({
size: sizeTest.name,
completed: false,
timeTaken,
timedOut: error.message === 'Timeout',
error: error.message
});
}
}
return results;
}
);
corruptedPerformance.forEach(result => {
t.ok(!result.timedOut,
`Corrupted file ${result.size} should not cause timeout`);
});
// Print performance summary
performanceTracker.printSummary();
});
// Helper functions
function createValidZipStructure(): Buffer {
// Simplified ZIP structure
const parts = [];
// Local file header
parts.push(Buffer.from('PK\x03\x04')); // Signature
parts.push(Buffer.alloc(26, 0)); // Header fields
parts.push(Buffer.from('test.xml')); // Filename
parts.push(Buffer.from('<Invoice><ID>123</ID></Invoice>')); // File data
// Central directory
parts.push(Buffer.from('PK\x01\x02')); // Signature
parts.push(Buffer.alloc(42, 0)); // Header fields
parts.push(Buffer.from('test.xml')); // Filename
// End of central directory
parts.push(Buffer.from('PK\x05\x06')); // Signature
parts.push(Buffer.alloc(18, 0)); // EOCD fields
return Buffer.concat(parts);
}
function createZipWithInvoice(): Buffer {
// Create a simple ZIP with invoice XML
return createValidZipStructure();
}
function findCRCOffset(data: Buffer): number {
// Find CRC32 field in ZIP structure
const sig = Buffer.from('PK\x03\x04');
const idx = data.indexOf(sig);
if (idx >= 0) {
return idx + 14; // CRC32 offset in local file header
}
return -1;
}
function createZipWithCompressionMethod(method: number, options: any): Buffer {
const parts = [];
// Local file header with specific compression method
parts.push(Buffer.from('PK\x03\x04'));
const header = Buffer.alloc(26, 0);
header.writeUInt16LE(method, 8); // Compression method
parts.push(header);
parts.push(Buffer.from('invoice.xml'));
// Add compressed or uncompressed data based on method
if (options.corrupted) {
parts.push(Buffer.from([0xFF, 0xFE, 0xFD])); // Invalid deflate stream
} else if (method === 0) {
parts.push(Buffer.from('<Invoice/>'));
} else {
parts.push(Buffer.from([0x78, 0x9C])); // Deflate header
parts.push(Buffer.alloc(10, 0)); // Compressed data
}
return Buffer.concat(parts);
}
function createZipBomb(config: any): Buffer {
// Create various types of ZIP bombs
if (config.circular) {
// Create a ZIP that references itself
return Buffer.from('PK...[circular reference]...');
} else if (config.depth) {
// Create nested ZIPs
let zip = Buffer.from('<Invoice/>');
for (let i = 0; i < config.depth; i++) {
zip = wrapInZip(zip, `level${i}.zip`);
}
return zip;
}
return Buffer.from('PK');
}
function wrapInZip(content: Buffer, filename: string): Buffer {
// Wrap content in a ZIP file
return Buffer.concat([
Buffer.from('PK\x03\x04'),
Buffer.alloc(26, 0),
Buffer.from(filename),
content
]);
}
function createZipWithFilename(filename: string, encoding: string): Buffer {
const parts = [];
parts.push(Buffer.from('PK\x03\x04'));
const header = Buffer.alloc(26, 0);
// Set filename length
const filenameBuffer = Buffer.from(filename, encoding === 'binary' ? 'binary' : 'utf8');
header.writeUInt16LE(filenameBuffer.length, 24);
parts.push(header);
parts.push(filenameBuffer);
parts.push(Buffer.from('<Invoice/>'));
return Buffer.concat(parts);
}
function createCorruptedFacturX(type: string): Buffer {
// Create corrupted Factur-X/ZUGFeRD PDFs
const mockPDF = Buffer.from('%PDF-1.4\n...');
return mockPDF;
}
function createSeverelyCorruptedZip(): Buffer {
// Create a severely corrupted ZIP for recovery testing
const data = Buffer.alloc(1024);
data.fill(0xFF);
// Add some ZIP-like signatures at random positions
data.write('PK\x03\x04', 100);
data.write('<Invoice', 200);
data.write('</Invoice>', 300);
return data;
}
function createMismatchedParts(): Buffer[] {
return [
Buffer.alloc(1000, 1),
Buffer.alloc(500, 2),
Buffer.alloc(1500, 3)
];
}
function createCorruptedZipOfSize(size: number): Buffer {
const data = Buffer.alloc(size);
// Fill with random data
for (let i = 0; i < size; i += 4) {
data.writeUInt32LE(Math.random() * 0xFFFFFFFF, i);
}
// Add ZIP signature at start
data.write('PK\x03\x04', 0);
return data;
}
function isValidXML(content: string): boolean {
try {
// Simple XML validation check
return content.includes('<?xml') && content.includes('>');
} catch {
return false;
}
}
// Run the test
tap.start();

View File

@ -0,0 +1,695 @@
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-10: Time Zone Edge Cases');
tap.test('EDGE-10: Time Zone Edge Cases - should handle complex timezone scenarios', async (t) => {
const einvoice = new EInvoice();
// Test 1: Date/time across timezone boundaries
const timezoneBoundaries = await performanceTracker.measureAsync(
'timezone-boundary-crossing',
async () => {
const boundaryTests = [
{
name: 'midnight-utc',
dateTime: '2024-01-15T00:00:00Z',
timezone: 'UTC',
expectedLocal: '2024-01-15T00:00:00'
},
{
name: 'midnight-cross-positive',
dateTime: '2024-01-15T23:59:59+12:00',
timezone: 'Pacific/Auckland',
expectedUTC: '2024-01-15T11:59:59Z'
},
{
name: 'midnight-cross-negative',
dateTime: '2024-01-15T00:00:00-11:00',
timezone: 'Pacific/Midway',
expectedUTC: '2024-01-15T11:00:00Z'
},
{
name: 'date-line-crossing',
dateTime: '2024-01-15T12:00:00+14:00',
timezone: 'Pacific/Kiritimati',
expectedUTC: '2024-01-14T22:00:00Z'
}
];
const results = [];
for (const test of boundaryTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>TZ-TEST-001</ID>
<IssueDate>${test.dateTime}</IssueDate>
<DueDateTime>${test.dateTime}</DueDateTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const dates = await einvoice.normalizeDates(parsed, {
targetTimezone: test.timezone
});
results.push({
test: test.name,
parsed: true,
originalDateTime: test.dateTime,
normalizedDate: dates?.IssueDate,
isDatePreserved: dates?.dateIntegrity || false,
crossesDateBoundary: dates?.crossesDateLine || false
});
} catch (error) {
results.push({
test: test.name,
parsed: false,
error: error.message
});
}
}
return results;
}
);
timezoneBoundaries.forEach(result => {
t.ok(result.parsed, `Timezone boundary ${result.test} should be handled`);
});
// Test 2: DST (Daylight Saving Time) transitions
const dstTransitions = await performanceTracker.measureAsync(
'dst-transition-handling',
async () => {
const dstTests = [
{
name: 'spring-forward-gap',
dateTime: '2024-03-10T02:30:00',
timezone: 'America/New_York',
description: 'Time that does not exist due to DST'
},
{
name: 'fall-back-ambiguous',
dateTime: '2024-11-03T01:30:00',
timezone: 'America/New_York',
description: 'Time that occurs twice due to DST'
},
{
name: 'dst-boundary-exact',
dateTime: '2024-03-31T02:00:00',
timezone: 'Europe/London',
description: 'Exact moment of DST transition'
},
{
name: 'southern-hemisphere-dst',
dateTime: '2024-10-06T02:00:00',
timezone: 'Australia/Sydney',
description: 'Southern hemisphere DST transition'
}
];
const results = [];
for (const test of dstTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>DST-${test.name}</ID>
<IssueDateTime>${test.dateTime}</IssueDateTime>
<ProcessingTime timezone="${test.timezone}">${test.dateTime}</ProcessingTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const dstAnalysis = await einvoice.analyzeDSTIssues(parsed);
results.push({
scenario: test.name,
handled: true,
hasAmbiguity: dstAnalysis?.isAmbiguous || false,
isNonExistent: dstAnalysis?.isNonExistent || false,
suggestion: dstAnalysis?.suggestion,
adjustedTime: dstAnalysis?.adjusted
});
} catch (error) {
results.push({
scenario: test.name,
handled: false,
error: error.message
});
}
}
return results;
}
);
dstTransitions.forEach(result => {
t.ok(result.handled, `DST transition ${result.scenario} should be handled`);
if (result.hasAmbiguity || result.isNonExistent) {
t.ok(result.suggestion, 'DST issue should have suggestion');
}
});
// Test 3: Historic timezone changes
const historicTimezones = await performanceTracker.measureAsync(
'historic-timezone-changes',
async () => {
const historicTests = [
{
name: 'pre-timezone-standardization',
dateTime: '1850-01-01T12:00:00',
location: 'Europe/London',
description: 'Before standard time zones'
},
{
name: 'soviet-time-changes',
dateTime: '1991-03-31T02:00:00',
location: 'Europe/Moscow',
description: 'USSR timezone reorganization'
},
{
name: 'samoa-dateline-change',
dateTime: '2011-12-30T00:00:00',
location: 'Pacific/Apia',
description: 'Samoa skipped December 30, 2011'
},
{
name: 'crimea-timezone-change',
dateTime: '2014-03-30T02:00:00',
location: 'Europe/Simferopol',
description: 'Crimea timezone change'
}
];
const results = [];
for (const test of historicTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>HISTORIC-${test.name}</ID>
<HistoricDate>${test.dateTime}</HistoricDate>
<Location>${test.location}</Location>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const historicAnalysis = await einvoice.handleHistoricDate(parsed, {
validateHistoric: true
});
results.push({
test: test.name,
processed: true,
isHistoric: historicAnalysis?.isHistoric || false,
hasTimezoneChange: historicAnalysis?.timezoneChanged || false,
warnings: historicAnalysis?.warnings || []
});
} catch (error) {
results.push({
test: test.name,
processed: false,
error: error.message
});
}
}
return results;
}
);
historicTimezones.forEach(result => {
t.ok(result.processed, `Historic timezone ${result.test} should be processed`);
});
// Test 4: Fractional timezone offsets
const fractionalTimezones = await performanceTracker.measureAsync(
'fractional-timezone-offsets',
async () => {
const fractionalTests = [
{
name: 'newfoundland-half-hour',
offset: '-03:30',
timezone: 'America/St_Johns',
dateTime: '2024-01-15T12:00:00-03:30'
},
{
name: 'india-half-hour',
offset: '+05:30',
timezone: 'Asia/Kolkata',
dateTime: '2024-01-15T12:00:00+05:30'
},
{
name: 'nepal-quarter-hour',
offset: '+05:45',
timezone: 'Asia/Kathmandu',
dateTime: '2024-01-15T12:00:00+05:45'
},
{
name: 'chatham-islands',
offset: '+12:45',
timezone: 'Pacific/Chatham',
dateTime: '2024-01-15T12:00:00+12:45'
}
];
const results = [];
for (const test of fractionalTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>FRAC-${test.name}</ID>
<IssueDateTime>${test.dateTime}</IssueDateTime>
<PaymentDueTime>${test.dateTime}</PaymentDueTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const normalized = await einvoice.normalizeToUTC(parsed);
results.push({
test: test.name,
offset: test.offset,
parsed: true,
correctlyHandled: normalized?.timezoneHandled || false,
preservedPrecision: normalized?.precisionMaintained || false
});
} catch (error) {
results.push({
test: test.name,
offset: test.offset,
parsed: false,
error: error.message
});
}
}
return results;
}
);
fractionalTimezones.forEach(result => {
t.ok(result.parsed, `Fractional timezone ${result.test} should be parsed`);
if (result.parsed) {
t.ok(result.correctlyHandled, 'Fractional offset should be handled correctly');
}
});
// Test 5: Missing or ambiguous timezone info
const ambiguousTimezones = await performanceTracker.measureAsync(
'ambiguous-timezone-info',
async () => {
const ambiguousTests = [
{
name: 'no-timezone-info',
xml: `<Invoice>
<IssueDate>2024-01-15</IssueDate>
<IssueTime>14:30:00</IssueTime>
</Invoice>`
},
{
name: 'conflicting-timezones',
xml: `<Invoice>
<IssueDateTime>2024-01-15T14:30:00+02:00</IssueDateTime>
<Timezone>America/New_York</Timezone>
</Invoice>`
},
{
name: 'local-time-only',
xml: `<Invoice>
<Timestamp>2024-01-15T14:30:00</Timestamp>
</Invoice>`
},
{
name: 'invalid-offset',
xml: `<Invoice>
<DateTime>2024-01-15T14:30:00+25:00</DateTime>
</Invoice>`
}
];
const results = [];
for (const test of ambiguousTests) {
const fullXml = `<?xml version="1.0" encoding="UTF-8"?>${test.xml}`;
try {
const parsed = await einvoice.parseXML(fullXml);
const timezoneAnalysis = await einvoice.resolveTimezones(parsed, {
defaultTimezone: 'UTC',
strict: false
});
results.push({
test: test.name,
resolved: true,
hasAmbiguity: timezoneAnalysis?.ambiguous || false,
resolution: timezoneAnalysis?.resolution,
confidence: timezoneAnalysis?.confidence || 0
});
} catch (error) {
results.push({
test: test.name,
resolved: false,
error: error.message
});
}
}
return results;
}
);
ambiguousTimezones.forEach(result => {
t.ok(result.resolved || result.error,
`Ambiguous timezone ${result.test} should be handled`);
if (result.resolved && result.hasAmbiguity) {
t.ok(result.confidence < 100, 'Ambiguous timezone should have lower confidence');
}
});
// Test 6: Leap seconds handling
const leapSeconds = await performanceTracker.measureAsync(
'leap-seconds-handling',
async () => {
const leapSecondTests = [
{
name: 'leap-second-23-59-60',
dateTime: '2016-12-31T23:59:60Z',
description: 'Actual leap second'
},
{
name: 'near-leap-second',
dateTime: '2016-12-31T23:59:59.999Z',
description: 'Just before leap second'
},
{
name: 'after-leap-second',
dateTime: '2017-01-01T00:00:00.001Z',
description: 'Just after leap second'
}
];
const results = [];
for (const test of leapSecondTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>LEAP-${test.name}</ID>
<PreciseTimestamp>${test.dateTime}</PreciseTimestamp>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const timeHandling = await einvoice.handlePreciseTime(parsed);
results.push({
test: test.name,
handled: true,
isLeapSecond: timeHandling?.isLeapSecond || false,
adjusted: timeHandling?.adjusted || false,
precision: timeHandling?.precision
});
} catch (error) {
results.push({
test: test.name,
handled: false,
error: error.message
});
}
}
return results;
}
);
leapSeconds.forEach(result => {
t.ok(result.handled || result.error,
`Leap second ${result.test} should be processed`);
});
// Test 7: Format-specific timezone handling
const formatSpecificTimezones = await performanceTracker.measureAsync(
'format-specific-timezone-handling',
async () => {
const formats = [
{
format: 'ubl',
xml: `<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<IssueDate>2024-01-15</IssueDate>
<IssueTime>14:30:00+02:00</IssueTime>
</Invoice>`
},
{
format: 'cii',
xml: `<rsm:CrossIndustryInvoice>
<rsm:ExchangedDocument>
<ram:IssueDateTime>
<udt:DateTimeString format="102">20240115143000</udt:DateTimeString>
</ram:IssueDateTime>
</rsm:ExchangedDocument>
</rsm:CrossIndustryInvoice>`
},
{
format: 'facturx',
xml: `<Invoice>
<IssueDateTime>2024-01-15T14:30:00</IssueDateTime>
<TimeZoneOffset>+0200</TimeZoneOffset>
</Invoice>`
}
];
const results = [];
for (const test of formats) {
try {
const parsed = await einvoice.parseDocument(test.xml);
const standardized = await einvoice.standardizeDateTime(parsed, {
sourceFormat: test.format
});
results.push({
format: test.format,
parsed: true,
hasDateTime: !!standardized?.dateTime,
hasTimezone: !!standardized?.timezone,
normalized: standardized?.normalized || false
});
} catch (error) {
results.push({
format: test.format,
parsed: false,
error: error.message
});
}
}
return results;
}
);
formatSpecificTimezones.forEach(result => {
t.ok(result.parsed, `Format ${result.format} timezone should be handled`);
});
// Test 8: Business day calculations across timezones
const businessDayCalculations = await performanceTracker.measureAsync(
'business-day-calculations',
async () => {
const businessTests = [
{
name: 'payment-terms-30-days',
issueDate: '2024-01-15T23:00:00+12:00',
terms: 30,
expectedDue: '2024-02-14'
},
{
name: 'cross-month-boundary',
issueDate: '2024-01-31T22:00:00-05:00',
terms: 1,
expectedDue: '2024-02-01'
},
{
name: 'weekend-adjustment',
issueDate: '2024-01-12T18:00:00Z', // Friday
terms: 3,
expectedDue: '2024-01-17' // Skip weekend
}
];
const results = [];
for (const test of businessTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>BUSINESS-${test.name}</ID>
<IssueDateTime>${test.issueDate}</IssueDateTime>
<PaymentTerms>
<NetDays>${test.terms}</NetDays>
</PaymentTerms>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const calculated = await einvoice.calculateDueDate(parsed, {
skipWeekends: true,
skipHolidays: true,
timezone: 'UTC'
});
results.push({
test: test.name,
calculated: true,
dueDate: calculated?.dueDate,
matchesExpected: calculated?.dueDate === test.expectedDue,
businessDaysUsed: calculated?.businessDays
});
} catch (error) {
results.push({
test: test.name,
calculated: false,
error: error.message
});
}
}
return results;
}
);
businessDayCalculations.forEach(result => {
t.ok(result.calculated, `Business day calculation ${result.test} should work`);
});
// Test 9: Timezone conversion errors
const timezoneConversionErrors = await performanceTracker.measureAsync(
'timezone-conversion-errors',
async () => {
const errorTests = [
{
name: 'invalid-timezone-name',
timezone: 'Invalid/Timezone',
dateTime: '2024-01-15T12:00:00'
},
{
name: 'deprecated-timezone',
timezone: 'US/Eastern', // Deprecated, use America/New_York
dateTime: '2024-01-15T12:00:00'
},
{
name: 'military-timezone',
timezone: 'Z', // Zulu time
dateTime: '2024-01-15T12:00:00'
},
{
name: 'three-letter-timezone',
timezone: 'EST', // Ambiguous
dateTime: '2024-01-15T12:00:00'
}
];
const results = [];
for (const test of errorTests) {
const xml = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>ERROR-${test.name}</ID>
<DateTime timezone="${test.timezone}">${test.dateTime}</DateTime>
</Invoice>`;
try {
const parsed = await einvoice.parseXML(xml);
const converted = await einvoice.convertTimezone(parsed, {
from: test.timezone,
to: 'UTC',
strict: true
});
results.push({
test: test.name,
handled: true,
converted: !!converted,
fallbackUsed: converted?.fallback || false,
warning: converted?.warning
});
} catch (error) {
results.push({
test: test.name,
handled: false,
error: error.message,
isTimezoneError: error.message.includes('timezone') ||
error.message.includes('time zone')
});
}
}
return results;
}
);
timezoneConversionErrors.forEach(result => {
t.ok(result.handled || result.isTimezoneError,
`Timezone error ${result.test} should be handled appropriately`);
});
// Test 10: Cross-format timezone preservation
const crossFormatTimezones = await performanceTracker.measureAsync(
'cross-format-timezone-preservation',
async () => {
const testData = {
dateTime: '2024-01-15T14:30:00+05:30',
timezone: 'Asia/Kolkata'
};
const sourceUBL = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<ID>TZ-PRESERVE-001</ID>
<IssueDate>2024-01-15</IssueDate>
<IssueTime>${testData.dateTime}</IssueTime>
</Invoice>`;
const conversions = ['cii', 'xrechnung', 'facturx'];
const results = [];
for (const targetFormat of conversions) {
try {
const converted = await einvoice.convertFormat(sourceUBL, targetFormat);
const reparsed = await einvoice.parseDocument(converted);
const extractedDateTime = await einvoice.extractDateTime(reparsed);
results.push({
targetFormat,
converted: true,
timezonePreserved: extractedDateTime?.timezone === testData.timezone,
offsetPreserved: extractedDateTime?.offset === '+05:30',
dateTimeIntact: extractedDateTime?.iso === testData.dateTime
});
} catch (error) {
results.push({
targetFormat,
converted: false,
error: error.message
});
}
}
return results;
}
);
crossFormatTimezones.forEach(result => {
t.ok(result.converted, `Conversion to ${result.targetFormat} should succeed`);
if (result.converted) {
t.ok(result.timezonePreserved || result.offsetPreserved,
'Timezone information should be preserved');
}
});
// Print performance summary
performanceTracker.printSummary();
});
// Run the test
tap.start();