einvoice/test/suite/einvoice_edge-cases/test.edge-01.empty-files.ts

461 lines
12 KiB
TypeScript
Raw Normal View History

2025-05-26 04:04:51 +00:00
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();