729 lines
21 KiB
TypeScript
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(); |