382 lines
11 KiB
TypeScript
382 lines
11 KiB
TypeScript
import { tap } from '@git.zone/tstest/tapbundle';
|
|
import { EInvoice } from '../../../ts/index.js';
|
|
import { PerformanceTracker } from '../performance.tracker.js';
|
|
|
|
tap.test('EDGE-05: Zero-Byte PDFs - should handle zero-byte and minimal PDF files', async () => {
|
|
// Test 1: Truly zero-byte PDF
|
|
await PerformanceTracker.track('truly-zero-byte-pdf', async () => {
|
|
const zeroPDF = Buffer.alloc(0);
|
|
|
|
try {
|
|
const result = await EInvoice.fromPdf(zeroPDF);
|
|
console.log('Zero-byte PDF: unexpectedly succeeded', result);
|
|
} catch (error) {
|
|
console.log('Zero-byte PDF: properly failed with error:', error.message);
|
|
}
|
|
});
|
|
|
|
// Test 2: Minimal PDF structure
|
|
await PerformanceTracker.track('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'
|
|
)
|
|
}
|
|
];
|
|
|
|
for (const pdf of minimalPDFs) {
|
|
try {
|
|
await EInvoice.fromPdf(pdf.content);
|
|
console.log(`Minimal PDF ${pdf.name}: size=${pdf.content.length}, extracted invoice`);
|
|
} catch (error) {
|
|
console.log(`Minimal PDF ${pdf.name}: failed - ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test 3: Truncated PDF files
|
|
await PerformanceTracker.track('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: 'before-eof', bytes: fullPDF.length - 5 }
|
|
];
|
|
|
|
for (const point of truncationPoints) {
|
|
const truncated = fullPDF.subarray(0, point.bytes);
|
|
|
|
try {
|
|
await EInvoice.fromPdf(truncated);
|
|
console.log(`Truncated PDF at ${point.name}: unexpectedly succeeded`);
|
|
} catch (error) {
|
|
console.log(`Truncated PDF at ${point.name}: properly failed - ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test 4: PDF extraction and embedding
|
|
await PerformanceTracker.track('pdf-extraction-embedding', async () => {
|
|
// Create an invoice first
|
|
const einvoice = new EInvoice();
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
einvoice.invoiceId = 'ZERO-001';
|
|
|
|
einvoice.from = {
|
|
type: 'company',
|
|
name: 'Test Company',
|
|
description: 'Testing zero-byte scenarios',
|
|
address: {
|
|
streetName: 'Test Street',
|
|
houseNumber: '1',
|
|
postalCode: '12345',
|
|
city: 'Test City',
|
|
country: 'DE'
|
|
},
|
|
status: 'active',
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
registrationDetails: {
|
|
vatId: 'DE123456789',
|
|
registrationId: 'HRB 12345',
|
|
registrationName: 'Commercial Register'
|
|
}
|
|
};
|
|
|
|
einvoice.to = {
|
|
type: 'person',
|
|
name: 'Test',
|
|
surname: 'Customer',
|
|
salutation: 'Mr' as const,
|
|
sex: 'male' as const,
|
|
title: 'Doctor' as const,
|
|
description: 'Test customer',
|
|
address: {
|
|
streetName: 'Customer Street',
|
|
houseNumber: '2',
|
|
postalCode: '54321',
|
|
city: 'Customer City',
|
|
country: 'DE'
|
|
}
|
|
};
|
|
|
|
einvoice.items = [{
|
|
position: 1,
|
|
name: 'Test Service',
|
|
articleNumber: 'SRV-001',
|
|
unitType: 'EA',
|
|
unitQuantity: 1,
|
|
unitNetPrice: 100,
|
|
vatPercentage: 19
|
|
}];
|
|
|
|
try {
|
|
// Generate UBL
|
|
const ublString = await einvoice.toXmlString('ubl');
|
|
console.log(`Generated UBL invoice: ${ublString.length} bytes`);
|
|
|
|
// Try to embed in a minimal PDF (this will likely fail)
|
|
const minimalPDF = Buffer.from('%PDF-1.4\n%%EOF');
|
|
await einvoice.embedInPdf(minimalPDF, 'ubl');
|
|
console.log(`Embedded XML in minimal PDF: success`);
|
|
} catch (error) {
|
|
console.log(`PDF embedding test failed: ${error.message}`);
|
|
}
|
|
});
|
|
|
|
// Test 5: Empty invoice edge cases
|
|
await PerformanceTracker.track('empty-invoice-edge-cases', async () => {
|
|
const testCases = [
|
|
{
|
|
name: 'no-items',
|
|
setup: (invoice: EInvoice) => {
|
|
invoice.items = [];
|
|
}
|
|
},
|
|
{
|
|
name: 'empty-strings',
|
|
setup: (invoice: EInvoice) => {
|
|
invoice.invoiceId = '';
|
|
invoice.items = [{
|
|
position: 1,
|
|
name: '',
|
|
articleNumber: '',
|
|
unitType: 'EA',
|
|
unitQuantity: 0,
|
|
unitNetPrice: 0,
|
|
vatPercentage: 0
|
|
}];
|
|
}
|
|
},
|
|
{
|
|
name: 'zero-amounts',
|
|
setup: (invoice: EInvoice) => {
|
|
invoice.items = [{
|
|
position: 1,
|
|
name: 'Zero Value Item',
|
|
articleNumber: 'ZERO-001',
|
|
unitType: 'EA',
|
|
unitQuantity: 0,
|
|
unitNetPrice: 0,
|
|
vatPercentage: 0
|
|
}];
|
|
}
|
|
}
|
|
];
|
|
|
|
for (const testCase of testCases) {
|
|
const einvoice = new EInvoice();
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
einvoice.invoiceId = 'EMPTY-001';
|
|
|
|
einvoice.from = {
|
|
type: 'company',
|
|
name: 'Empty Test Company',
|
|
description: 'Testing empty scenarios',
|
|
address: {
|
|
streetName: 'Test Street',
|
|
houseNumber: '1',
|
|
postalCode: '12345',
|
|
city: 'Test City',
|
|
country: 'DE'
|
|
},
|
|
status: 'active',
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
registrationDetails: {
|
|
vatId: 'DE123456789',
|
|
registrationId: 'HRB 12345',
|
|
registrationName: 'Commercial Register'
|
|
}
|
|
};
|
|
|
|
einvoice.to = {
|
|
type: 'company',
|
|
name: 'Customer Company',
|
|
description: 'Customer',
|
|
address: {
|
|
streetName: 'Customer Street',
|
|
houseNumber: '2',
|
|
postalCode: '54321',
|
|
city: 'Customer City',
|
|
country: 'DE'
|
|
},
|
|
status: 'active',
|
|
foundedDate: { year: 2019, month: 1, day: 1 },
|
|
registrationDetails: {
|
|
vatId: 'DE987654321',
|
|
registrationId: 'HRB 54321',
|
|
registrationName: 'Commercial Register'
|
|
}
|
|
};
|
|
|
|
// Apply test-specific setup
|
|
testCase.setup(einvoice);
|
|
|
|
try {
|
|
const ciiString = await einvoice.toXmlString('cii');
|
|
console.log(`Empty test ${testCase.name}: generated ${ciiString.length} bytes`);
|
|
|
|
// Try validation
|
|
const validationResult = await einvoice.validate();
|
|
console.log(`Empty test ${testCase.name} validation: ${validationResult.valid ? 'valid' : 'invalid'}`);
|
|
if (!validationResult.valid) {
|
|
console.log(`Validation errors: ${validationResult.errors.length}`);
|
|
}
|
|
} catch (error) {
|
|
console.log(`Empty test ${testCase.name} failed: ${error.message}`);
|
|
}
|
|
}
|
|
});
|
|
|
|
// Test 6: Batch processing with zero-byte PDFs
|
|
await PerformanceTracker.track('batch-processing-zero-byte', async () => {
|
|
const batch = [
|
|
{ name: 'zero-byte', content: Buffer.alloc(0) },
|
|
{ name: 'header-only', content: Buffer.from('%PDF-1.4') },
|
|
{ name: 'invalid', content: Buffer.from('Not a PDF') },
|
|
{ name: 'valid-minimal', content: createMinimalValidPDF() }
|
|
];
|
|
|
|
let successful = 0;
|
|
let failed = 0;
|
|
|
|
for (const item of batch) {
|
|
try {
|
|
await EInvoice.fromPdf(item.content);
|
|
successful++;
|
|
console.log(`Batch item ${item.name}: extracted successfully`);
|
|
} catch (error) {
|
|
failed++;
|
|
console.log(`Batch item ${item.name}: failed - ${error.message}`);
|
|
}
|
|
}
|
|
|
|
console.log(`Batch processing complete: ${successful} successful, ${failed} failed`);
|
|
});
|
|
|
|
// Test 7: Memory efficiency with zero content
|
|
await PerformanceTracker.track('memory-efficiency-zero-content', async () => {
|
|
const iterations = 100;
|
|
const beforeMem = process.memoryUsage();
|
|
|
|
// Create many empty invoices
|
|
const invoices: EInvoice[] = [];
|
|
for (let i = 0; i < iterations; i++) {
|
|
const einvoice = new EInvoice();
|
|
einvoice.issueDate = new Date(2024, 0, 1);
|
|
einvoice.invoiceId = `MEM-${i}`;
|
|
|
|
einvoice.from = {
|
|
type: 'company',
|
|
name: 'Memory Test',
|
|
description: 'Testing memory',
|
|
address: {
|
|
streetName: 'Test Street',
|
|
houseNumber: '1',
|
|
postalCode: '12345',
|
|
city: 'Test City',
|
|
country: 'DE'
|
|
},
|
|
status: 'active',
|
|
foundedDate: { year: 2020, month: 1, day: 1 },
|
|
registrationDetails: {
|
|
vatId: 'DE123456789',
|
|
registrationId: 'HRB 12345',
|
|
registrationName: 'Commercial Register'
|
|
}
|
|
};
|
|
|
|
einvoice.to = {
|
|
type: 'person',
|
|
name: 'Test',
|
|
surname: 'Customer',
|
|
salutation: 'Mr' as const,
|
|
sex: 'male' as const,
|
|
title: 'Doctor' as const,
|
|
description: 'Test customer',
|
|
address: {
|
|
streetName: 'Customer Street',
|
|
houseNumber: '2',
|
|
postalCode: '54321',
|
|
city: 'Customer City',
|
|
country: 'DE'
|
|
}
|
|
};
|
|
|
|
einvoice.items = []; // Empty items
|
|
invoices.push(einvoice);
|
|
}
|
|
|
|
const afterMem = process.memoryUsage();
|
|
const memDiff = {
|
|
heapUsed: Math.round((afterMem.heapUsed - beforeMem.heapUsed) / 1024 / 1024 * 100) / 100,
|
|
rss: Math.round((afterMem.rss - beforeMem.rss) / 1024 / 1024 * 100) / 100
|
|
};
|
|
|
|
console.log(`Created ${iterations} empty invoices`);
|
|
console.log(`Memory usage increase: Heap: ${memDiff.heapUsed}MB, RSS: ${memDiff.rss}MB`);
|
|
|
|
// Try to process them all
|
|
let processedCount = 0;
|
|
for (const invoice of invoices) {
|
|
try {
|
|
const xml = await invoice.toXmlString('ubl');
|
|
if (xml && xml.length > 0) {
|
|
processedCount++;
|
|
}
|
|
} catch (error) {
|
|
// Expected for empty invoices
|
|
}
|
|
}
|
|
|
|
console.log(`Successfully processed ${processedCount} out of ${iterations} empty invoices`);
|
|
});
|
|
});
|
|
|
|
// Helper function to create a minimal valid PDF
|
|
function createMinimalValidPDF(): Buffer {
|
|
return 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'
|
|
);
|
|
}
|
|
|
|
// Run the test
|
|
tap.start(); |