fix(compliance): improve compliance

This commit is contained in:
2025-05-28 14:46:32 +00:00
parent 784a50bc7f
commit 16e2bd6b1a
16 changed files with 4718 additions and 3138 deletions

View File

@ -1,22 +1,27 @@
import { tap } from '@git.zone/tstest/tapbundle';
import { expect, 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 () => {
console.log('Testing zero-byte and minimal PDF handling...\n');
// Test 1: Truly zero-byte PDF
await PerformanceTracker.track('truly-zero-byte-pdf', async () => {
const testZeroBytePdf = async () => {
const zeroPDF = Buffer.alloc(0);
try {
const result = await EInvoice.fromPdf(zeroPDF);
console.log('Zero-byte PDF: unexpectedly succeeded', result);
console.log('Test 1 - Zero-byte PDF:');
console.log(' Unexpectedly succeeded, result:', result);
return { handled: false, error: null };
} catch (error) {
console.log('Zero-byte PDF: properly failed with error:', error.message);
console.log('Test 1 - Zero-byte PDF:');
console.log(' Properly failed with error:', error.message);
return { handled: true, error: error.message };
}
});
};
// Test 2: Minimal PDF structure
await PerformanceTracker.track('minimal-pdf-structure', async () => {
// Test 2: Minimal PDF structures
const testMinimalPdfStructures = async () => {
const minimalPDFs = [
{
name: 'header-only',
@ -37,346 +42,189 @@ tap.test('EDGE-05: Zero-Byte PDFs - should handle zero-byte and minimal PDF file
'trailer\n<< /Size 2 /Root 1 0 R >>\n' +
'startxref\n64\n%%EOF'
)
},
{
name: 'invalid-header',
content: Buffer.from('NOT-A-PDF-HEADER')
},
{
name: 'truncated-pdf',
content: Buffer.from('%PDF-1.4\n1 0 obj\n<< /Type /Cat')
}
];
const results = [];
for (const pdf of minimalPDFs) {
try {
const result = await EInvoice.fromPdf(pdf.content);
console.log(`\nTest 2.${pdf.name}:`);
console.log(` Size: ${pdf.content.length} bytes`);
console.log(` Extracted invoice: Yes`);
console.log(` Result type: ${typeof result}`);
results.push({ name: pdf.name, success: true, error: null });
} catch (error) {
console.log(`\nTest 2.${pdf.name}:`);
console.log(` Size: ${pdf.content.length} bytes`);
console.log(` Error: ${error.message}`);
results.push({ name: pdf.name, success: false, error: error.message });
}
}
return results;
};
// Test 3: PDF with invalid content but correct headers
const testInvalidContentPdf = async () => {
const invalidContentPDFs = [
{
name: 'binary-garbage',
content: Buffer.concat([
Buffer.from('%PDF-1.4\n'),
Buffer.from(Array(100).fill(0).map(() => Math.floor(Math.random() * 256))),
Buffer.from('\n%%EOF')
])
},
{
name: 'text-only',
content: Buffer.from('%PDF-1.4\nThis is just plain text content\n%%EOF')
},
{
name: 'xml-content',
content: Buffer.from('%PDF-1.4\n<xml><invoice>test</invoice></xml>\n%%EOF')
}
];
const results = [];
for (const pdf of invalidContentPDFs) {
try {
const result = await EInvoice.fromPdf(pdf.content);
console.log(`\nTest 3.${pdf.name}:`);
console.log(` PDF parsed successfully: Yes`);
console.log(` Invoice extracted: ${result ? 'Yes' : 'No'}`);
results.push({ name: pdf.name, parsed: true, extracted: !!result });
} catch (error) {
console.log(`\nTest 3.${pdf.name}:`);
console.log(` Error: ${error.message}`);
results.push({ name: pdf.name, parsed: false, extracted: false, error: error.message });
}
}
return results;
};
// Test 4: Edge case PDF sizes
const testEdgeCaseSizes = async () => {
const edgeCasePDFs = [
{
name: 'single-byte',
content: Buffer.from('P')
},
{
name: 'minimal-header',
content: Buffer.from('%PDF')
},
{
name: 'almost-valid-header',
content: Buffer.from('%PDF-1')
},
{
name: 'very-large-empty',
content: Buffer.concat([
Buffer.from('%PDF-1.4\n'),
Buffer.alloc(10000, 0x20), // 10KB of spaces
Buffer.from('\n%%EOF')
])
}
];
const results = [];
for (const pdf of edgeCasePDFs) {
try {
await EInvoice.fromPdf(pdf.content);
console.log(`Minimal PDF ${pdf.name}: size=${pdf.content.length}, extracted invoice`);
console.log(`\nTest 4.${pdf.name}:`);
console.log(` Size: ${pdf.content.length} bytes`);
console.log(` Processing successful: Yes`);
results.push({ name: pdf.name, size: pdf.content.length, processed: true });
} catch (error) {
console.log(`Minimal PDF ${pdf.name}: failed - ${error.message}`);
console.log(`\nTest 4.${pdf.name}:`);
console.log(` Size: ${pdf.content.length} bytes`);
console.log(` Error: ${error.message}`);
results.push({ name: pdf.name, size: pdf.content.length, processed: false, error: error.message });
}
}
});
return results;
};
// 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
}];
// Test 5: PDF with embedded XML but malformed structure
const testMalformedEmbeddedXml = async () => {
try {
// Generate UBL
const ublString = await einvoice.toXmlString('ubl');
console.log(`Generated UBL invoice: ${ublString.length} bytes`);
// Create a PDF-like structure with embedded XML-like content
const malformedPdf = Buffer.from(
'%PDF-1.4\n' +
'1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n' +
'2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n' +
'3 0 obj\n<< /Type /Page /Parent 2 0 R >>\nendobj\n' +
'4 0 obj\n<< /Type /EmbeddedFile /Filter /ASCIIHexDecode /Length 100 >>\n' +
'stream\n' +
'3C696E766F6963653E3C2F696E766F6963653E\n' + // hex for <invoice></invoice>
'endstream\nendobj\n' +
'xref\n0 5\n' +
'0000000000 65535 f\n' +
'0000000009 00000 n\n' +
'0000000052 00000 n\n' +
'0000000101 00000 n\n' +
'0000000141 00000 n\n' +
'trailer\n<< /Size 5 /Root 1 0 R >>\n' +
'startxref\n241\n%%EOF'
);
const result = await EInvoice.fromPdf(malformedPdf);
// 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`);
console.log(`\nTest 5 - Malformed embedded XML:`);
console.log(` PDF size: ${malformedPdf.length} bytes`);
console.log(` Processing result: ${result ? 'Success' : 'No invoice found'}`);
return { processed: true, result: !!result };
} catch (error) {
console.log(`PDF embedding test failed: ${error.message}`);
console.log(`\nTest 5 - Malformed embedded XML:`);
console.log(` Error: ${error.message}`);
return { processed: false, error: 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}`);
}
}
});
// Run all tests
const zeroByteResult = await testZeroBytePdf();
const minimalResults = await testMinimalPdfStructures();
const invalidContentResults = await testInvalidContentPdf();
const edgeCaseResults = await testEdgeCaseSizes();
const malformedResult = await testMalformedEmbeddedXml();
// 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`);
});
console.log(`\n=== Zero-Byte PDF Test Summary ===`);
// Count results
const minimalHandled = minimalResults.filter(r => r.error !== null).length;
const invalidHandled = invalidContentResults.filter(r => r.error !== null).length;
const edgeCaseHandled = edgeCaseResults.filter(r => r.error !== null).length;
console.log(`Zero-byte PDF: ${zeroByteResult.handled ? 'Properly handled' : 'Unexpected behavior'}`);
console.log(`Minimal PDFs: ${minimalHandled}/${minimalResults.length} properly handled`);
console.log(`Invalid content PDFs: ${invalidHandled}/${invalidContentResults.length} properly handled`);
console.log(`Edge case sizes: ${edgeCaseHandled}/${edgeCaseResults.length} properly handled`);
console.log(`Malformed embedded XML: ${malformedResult.processed ? 'Processed' : 'Error handled'}`);
// 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`);
});
// Test passes if the library properly handles edge cases without crashing
// Zero-byte PDF should fail gracefully
expect(zeroByteResult.handled).toBeTrue();
// At least some minimal PDFs should fail (they don't contain valid invoice data)
const someMinimalFailed = minimalResults.some(r => !r.success);
expect(someMinimalFailed).toBeTrue();
});
// 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();