einvoice/test/suite/einvoice_edge-cases/test.edge-07.max-field-lengths.ts
2025-05-26 04:04:51 +00:00

729 lines
21 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-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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>TEST</ID>
<CustomerName>${value}</CustomerName>
</Invoice>`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>EXTREME-${testCase.name}</ID>
<LongField>${value}</LongField>
</Invoice>`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>ATT-TEST</ID>
<Attachment>
<EmbeddedDocumentBinaryObject mimeCode="application/pdf">
${base64Data}
</EmbeddedDocumentBinaryObject>
</Attachment>
</Invoice>`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<TotalAmount currencyID="EUR">${test.value}</TotalAmount>
<Items>
<Item>
<Price>${test.value}</Price>
<Quantity>1</Quantity>
</Item>
</Items>
</Invoice>`;
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 += `<Level${i}><Field${i}>${fieldValue}</Field${i}>`;
}
xml += '<Core>Data</Core>';
for (let i = depth - 1; i >= 0; i--) {
xml += `</Level${i}>`;
}
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>${content}</Invoice>`;
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 = `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>PERF-TEST</ID>
<Description>${value}</Description>
<Note>${value}</Note>
<CustomerName>${value}</CustomerName>
</Invoice>`;
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': `<ID>${value}</ID>`,
'CustomerName': `<CustomerName>${value}</CustomerName>`,
'Description': `<Description>${value}</Description>`,
'Note': `<Note>${value}</Note>`,
'Reference': `<Reference>${value}</Reference>`,
'Email': `<Email>${value}</Email>`,
'Phone': `<Phone>${value}</Phone>`,
'PostalCode': `<PostalCode>${value}</PostalCode>`
};
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>TEST-001</ID>
${fieldMap[field] || `<${field}>${value}</${field}>`}
</Invoice>`;
}
// 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 `<?xml version="1.0" encoding="UTF-8"?>
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2">
<${field}>${value}</${field}>
</Invoice>`;
} else if (format === 'cii') {
return `<?xml version="1.0" encoding="UTF-8"?>
<rsm:CrossIndustryInvoice xmlns:rsm="urn:un:unece:uncefact:data:standard:CrossIndustryInvoice:100">
<rsm:${field}>${value}</rsm:${field}>
</rsm:CrossIndustryInvoice>`;
}
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 += `<Item><ID>${i}</ID><Price>10.00</Price></Item>`;
}
return `<?xml version="1.0" encoding="UTF-8"?>
<Invoice>
<ID>MANY-ITEMS</ID>
<Items>${items}</Items>
</Invoice>`;
}
// 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();