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 = [ '', '\n', '', '', '', '' ]; 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: ` 2024-01-01 ` }, { name: 'whitespace-id', xml: ` 2024-01-01 ` }, { name: 'empty-amount', xml: ` INV-001 ` } ]; 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: ` INV-001 ` }, { name: 'empty-tax-totals', xml: ` INV-001 ` } ]; 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<>\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: '', 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: '' }, { name: 'empty-prefix-namespace', xml: '' }, { name: 'whitespace-namespace', xml: '' } ]; 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( 'TEST' ); results.normalAfterEmpty = !!normal; } catch (error) { // Should not happen } // Test 3: Batch with empty file try { const batch = await einvoice.batchProcess([ '1', '', '2' ]); 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();