461 lines
12 KiB
TypeScript
461 lines
12 KiB
TypeScript
|
import { tap } from '@git.zone/tstest/tapbundle';
|
||
|
import * as plugins from '../plugins.js';
|
||
|
import { EInvoice } from '../../../ts/index.js';
|
||
|
import { PerformanceTracker } from '../performance.tracker.js';
|
||
|
|
||
|
const performanceTracker = new PerformanceTracker('EDGE-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();
|