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-07: Maximum Field Lengths'); tap.test('EDGE-07: Maximum Field Lengths - should handle fields at maximum allowed lengths', async (t) => { const einvoice = new EInvoice(); // Test 1: Standard field length limits const standardFieldLimits = await performanceTracker.measureAsync( 'standard-field-limits', async () => { const fieldTests = [ { field: 'InvoiceID', maxLength: 200, standard: 'EN16931' }, { field: 'CustomerName', maxLength: 200, standard: 'EN16931' }, { field: 'Description', maxLength: 1000, standard: 'EN16931' }, { field: 'Note', maxLength: 5000, standard: 'EN16931' }, { field: 'Reference', maxLength: 200, standard: 'EN16931' }, { field: 'Email', maxLength: 254, standard: 'RFC5321' }, { field: 'Phone', maxLength: 30, standard: 'ITU-T' }, { field: 'PostalCode', maxLength: 20, standard: 'UPU' } ]; const results = []; for (const test of fieldTests) { // Test at max length const maxValue = 'X'.repeat(test.maxLength); const xml = createInvoiceWithField(test.field, maxValue); try { const parsed = await einvoice.parseXML(xml); const validated = await einvoice.validate(parsed); results.push({ field: test.field, length: test.maxLength, parsed: true, valid: validated?.isValid || false, preserved: getFieldValue(parsed, test.field)?.length === test.maxLength }); } catch (error) { results.push({ field: test.field, length: test.maxLength, parsed: false, error: error.message }); } // Test over max length const overValue = 'X'.repeat(test.maxLength + 1); const overXml = createInvoiceWithField(test.field, overValue); try { const parsed = await einvoice.parseXML(overXml); const validated = await einvoice.validate(parsed); results.push({ field: test.field, length: test.maxLength + 1, parsed: true, valid: validated?.isValid || false, truncated: getFieldValue(parsed, test.field)?.length <= test.maxLength }); } catch (error) { results.push({ field: test.field, length: test.maxLength + 1, parsed: false, error: error.message }); } } return results; } ); standardFieldLimits.forEach(result => { if (result.length <= result.maxLength) { t.ok(result.valid, `Field ${result.field} at max length should be valid`); } else { t.notOk(result.valid, `Field ${result.field} over max length should be invalid`); } }); // Test 2: Unicode character length vs byte length const unicodeLengthTests = await performanceTracker.measureAsync( 'unicode-length-vs-bytes', async () => { const testCases = [ { name: 'ascii-only', char: 'A', bytesPerChar: 1 }, { name: 'latin-extended', char: 'ñ', bytesPerChar: 2 }, { name: 'chinese', char: '中', bytesPerChar: 3 }, { name: 'emoji', char: '😀', bytesPerChar: 4 } ]; const results = []; const maxChars = 100; for (const test of testCases) { const value = test.char.repeat(maxChars); const byteLength = Buffer.from(value, 'utf8').length; const xml = ` TEST ${value} `; try { const parsed = await einvoice.parseXML(xml); const retrievedValue = parsed?.CustomerName || ''; results.push({ type: test.name, charCount: value.length, byteCount: byteLength, expectedBytes: maxChars * test.bytesPerChar, preserved: retrievedValue === value, retrievedLength: retrievedValue.length, retrievedBytes: Buffer.from(retrievedValue, 'utf8').length }); } catch (error) { results.push({ type: test.name, charCount: value.length, byteCount: byteLength, error: error.message }); } } return results; } ); unicodeLengthTests.forEach(result => { t.ok(result.preserved || result.error, `Unicode ${result.type} field should be handled correctly`); if (result.preserved) { t.equal(result.retrievedLength, result.charCount, `Character count should be preserved for ${result.type}`); } }); // Test 3: Format-specific field limits const formatSpecificLimits = await performanceTracker.measureAsync( 'format-specific-limits', async () => { const formatLimits = [ { format: 'ubl', fields: [ { name: 'ID', maxLength: 200 }, { name: 'Note', maxLength: 1000 }, { name: 'DocumentCurrencyCode', maxLength: 3 } ] }, { format: 'cii', fields: [ { name: 'ID', maxLength: 35 }, { name: 'Content', maxLength: 5000 }, { name: 'TypeCode', maxLength: 4 } ] }, { format: 'xrechnung', fields: [ { name: 'BT-1', maxLength: 16 }, // Invoice number { name: 'BT-22', maxLength: 1000 }, // Note { name: 'BT-5', maxLength: 3 } // Currency ] } ]; const results = []; for (const format of formatLimits) { for (const field of format.fields) { const value = 'A'.repeat(field.maxLength); const invoice = createFormatSpecificInvoice(format.format, field.name, value); try { const parsed = await einvoice.parseDocument(invoice); const validated = await einvoice.validateFormat(parsed, format.format); results.push({ format: format.format, field: field.name, maxLength: field.maxLength, valid: validated?.isValid || false, compliant: validated?.formatCompliant || false }); } catch (error) { results.push({ format: format.format, field: field.name, maxLength: field.maxLength, error: error.message }); } } } return results; } ); formatSpecificLimits.forEach(result => { t.ok(result.valid || result.error, `${result.format} field ${result.field} at max length was processed`); }); // Test 4: Extreme length edge cases const extremeLengthCases = await performanceTracker.measureAsync( 'extreme-length-edge-cases', async () => { const extremeCases = [ { length: 0, name: 'empty' }, { length: 1, name: 'single-char' }, { length: 255, name: 'common-db-limit' }, { length: 65535, name: 'uint16-max' }, { length: 1000000, name: 'one-million' }, { length: 10000000, name: 'ten-million' } ]; const results = []; for (const testCase of extremeCases) { const value = testCase.length > 0 ? 'X'.repeat(testCase.length) : ''; const xml = ` EXTREME-${testCase.name} ${value} `; const startTime = Date.now(); const startMemory = process.memoryUsage(); try { const parsed = await einvoice.parseXML(xml); const endTime = Date.now(); const endMemory = process.memoryUsage(); results.push({ length: testCase.length, name: testCase.name, parsed: true, timeTaken: endTime - startTime, memoryUsed: endMemory.heapUsed - startMemory.heapUsed, fieldPreserved: parsed?.LongField?.length === testCase.length }); } catch (error) { results.push({ length: testCase.length, name: testCase.name, parsed: false, error: error.message, isLengthError: error.message.includes('length') || error.message.includes('size') }); } } return results; } ); extremeLengthCases.forEach(result => { if (result.length <= 65535) { t.ok(result.parsed, `Length ${result.name} should be handled`); } else { t.ok(!result.parsed || result.isLengthError, `Extreme length ${result.name} should be limited`); } }); // Test 5: Line item count limits const lineItemCountLimits = await performanceTracker.measureAsync( 'line-item-count-limits', async () => { const itemCounts = [100, 1000, 9999, 10000, 99999]; const results = []; for (const count of itemCounts) { const invoice = createInvoiceWithManyItems(count); const startTime = Date.now(); try { const parsed = await einvoice.parseXML(invoice); const itemsParsed = countItems(parsed); const endTime = Date.now(); results.push({ requestedCount: count, parsedCount: itemsParsed, success: true, timeTaken: endTime - startTime, avgTimePerItem: (endTime - startTime) / count }); } catch (error) { results.push({ requestedCount: count, success: false, error: error.message }); } } return results; } ); lineItemCountLimits.forEach(result => { if (result.requestedCount <= 10000) { t.ok(result.success, `${result.requestedCount} line items should be supported`); } }); // Test 6: Attachment size limits const attachmentSizeLimits = await performanceTracker.measureAsync( 'attachment-size-limits', async () => { const sizes = [ { size: 1024 * 1024, name: '1MB' }, { size: 10 * 1024 * 1024, name: '10MB' }, { size: 50 * 1024 * 1024, name: '50MB' }, { size: 100 * 1024 * 1024, name: '100MB' } ]; const results = []; for (const test of sizes) { const attachmentData = Buffer.alloc(test.size, 'A'); const base64Data = attachmentData.toString('base64'); const xml = ` ATT-TEST ${base64Data} `; try { const parsed = await einvoice.parseXML(xml); const attachment = extractAttachment(parsed); results.push({ size: test.name, bytes: test.size, parsed: true, attachmentPreserved: attachment?.length === test.size }); } catch (error) { results.push({ size: test.name, bytes: test.size, parsed: false, error: error.message }); } } return results; } ); attachmentSizeLimits.forEach(result => { if (result.bytes <= 50 * 1024 * 1024) { t.ok(result.parsed, `Attachment size ${result.size} should be supported`); } }); // Test 7: Decimal precision limits const decimalPrecisionLimits = await performanceTracker.measureAsync( 'decimal-precision-limits', async () => { const precisionTests = [ { decimals: 2, value: '12345678901234567890.12' }, { decimals: 4, value: '123456789012345678.1234' }, { decimals: 6, value: '1234567890123456.123456' }, { decimals: 10, value: '123456789012.1234567890' }, { decimals: 20, value: '12.12345678901234567890' }, { decimals: 30, value: '1.123456789012345678901234567890' } ]; const results = []; for (const test of precisionTests) { const xml = ` ${test.value} ${test.value} 1 `; try { const parsed = await einvoice.parseXML(xml); const amount = parsed?.TotalAmount; // Check precision preservation const preserved = amount?.toString() === test.value; const rounded = amount?.toString() !== test.value; results.push({ decimals: test.decimals, originalValue: test.value, parsedValue: amount?.toString(), preserved, rounded }); } catch (error) { results.push({ decimals: test.decimals, error: error.message }); } } return results; } ); decimalPrecisionLimits.forEach(result => { t.ok(result.preserved || result.rounded, `Decimal precision ${result.decimals} should be handled`); }); // Test 8: Maximum nesting with field lengths const nestingWithLengths = await performanceTracker.measureAsync( 'nesting-with-field-lengths', async () => { const createDeepStructure = (depth: number, fieldLength: number) => { let xml = ''; const fieldValue = 'X'.repeat(fieldLength); for (let i = 0; i < depth; i++) { xml += `${fieldValue}`; } xml += 'Data'; for (let i = depth - 1; i >= 0; i--) { xml += ``; } return xml; }; const tests = [ { depth: 10, fieldLength: 1000 }, { depth: 50, fieldLength: 100 }, { depth: 100, fieldLength: 10 }, { depth: 5, fieldLength: 10000 } ]; const results = []; for (const test of tests) { const content = createDeepStructure(test.depth, test.fieldLength); const xml = ` ${content}`; const totalDataSize = test.depth * test.fieldLength; try { const startTime = Date.now(); const parsed = await einvoice.parseXML(xml); const endTime = Date.now(); results.push({ depth: test.depth, fieldLength: test.fieldLength, totalDataSize, parsed: true, timeTaken: endTime - startTime }); } catch (error) { results.push({ depth: test.depth, fieldLength: test.fieldLength, totalDataSize, parsed: false, error: error.message }); } } return results; } ); nestingWithLengths.forEach(result => { t.ok(result.parsed || result.error, `Nested structure with depth ${result.depth} and field length ${result.fieldLength} was processed`); }); // Test 9: Field truncation behavior const fieldTruncationBehavior = await performanceTracker.measureAsync( 'field-truncation-behavior', async () => { const truncationTests = [ { field: 'ID', maxLength: 50, testValue: 'A'.repeat(100), truncationType: 'hard' }, { field: 'Note', maxLength: 1000, testValue: 'B'.repeat(2000), truncationType: 'soft' }, { field: 'Email', maxLength: 254, testValue: 'x'.repeat(250) + '@test.com', truncationType: 'smart' } ]; const results = []; for (const test of truncationTests) { const xml = createInvoiceWithField(test.field, test.testValue); try { const parsed = await einvoice.parseXML(xml, { truncateFields: true, truncationMode: test.truncationType }); const fieldValue = getFieldValue(parsed, test.field); results.push({ field: test.field, originalLength: test.testValue.length, truncatedLength: fieldValue?.length || 0, truncated: fieldValue?.length < test.testValue.length, withinLimit: fieldValue?.length <= test.maxLength, truncationType: test.truncationType }); } catch (error) { results.push({ field: test.field, error: error.message }); } } return results; } ); fieldTruncationBehavior.forEach(result => { if (result.truncated) { t.ok(result.withinLimit, `Field ${result.field} should be truncated to within limit`); } }); // Test 10: Performance impact of field lengths const performanceImpact = await performanceTracker.measureAsync( 'field-length-performance-impact', async () => { const lengths = [10, 100, 1000, 10000, 100000]; const results = []; for (const length of lengths) { const iterations = 10; const times = []; for (let i = 0; i < iterations; i++) { const value = 'X'.repeat(length); const xml = ` PERF-TEST ${value} ${value} ${value} `; const startTime = process.hrtime.bigint(); try { await einvoice.parseXML(xml); } catch (error) { // Ignore errors for performance testing } const endTime = process.hrtime.bigint(); times.push(Number(endTime - startTime) / 1000000); // Convert to ms } const avgTime = times.reduce((a, b) => a + b, 0) / times.length; results.push({ fieldLength: length, avgParseTime: avgTime, timePerKB: avgTime / (length * 3 / 1024) // 3 fields }); } return results; } ); // Verify performance doesn't degrade exponentially const timeRatios = performanceImpact.map((r, i) => i > 0 ? r.avgParseTime / performanceImpact[i-1].avgParseTime : 1 ); timeRatios.forEach((ratio, i) => { if (i > 0) { t.ok(ratio < 15, `Performance scaling should be reasonable at length ${performanceImpact[i].fieldLength}`); } }); // Print performance summary performanceTracker.printSummary(); }); // Helper function to create invoice with specific field function createInvoiceWithField(field: string, value: string): string { const fieldMap = { 'InvoiceID': `${value}`, 'CustomerName': `${value}`, 'Description': `${value}`, 'Note': `${value}`, 'Reference': `${value}`, 'Email': `${value}`, 'Phone': `${value}`, 'PostalCode': `${value}` }; return ` TEST-001 ${fieldMap[field] || `<${field}>${value}`} `; } // Helper function to get field value from parsed object function getFieldValue(parsed: any, field: string): string | undefined { return parsed?.[field] || parsed?.Invoice?.[field]; } // Helper function to create format-specific invoice function createFormatSpecificInvoice(format: string, field: string, value: string): string { if (format === 'ubl') { return ` <${field}>${value} `; } else if (format === 'cii') { return ` ${value} `; } return createInvoiceWithField(field, value); } // Helper function to create invoice with many items function createInvoiceWithManyItems(count: number): string { let items = ''; for (let i = 0; i < count; i++) { items += `${i}10.00`; } return ` MANY-ITEMS ${items} `; } // Helper function to count items function countItems(parsed: any): number { if (!parsed?.Items) return 0; if (Array.isArray(parsed.Items)) return parsed.Items.length; if (parsed.Items.Item) { return Array.isArray(parsed.Items.Item) ? parsed.Items.Item.length : 1; } return 0; } // Helper function to extract attachment function extractAttachment(parsed: any): Buffer | null { const base64Data = parsed?.Attachment?.EmbeddedDocumentBinaryObject; if (base64Data) { return Buffer.from(base64Data, 'base64'); } return null; } // Run the test tap.start();