239 lines
8.3 KiB
TypeScript
239 lines
8.3 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
|
import { connectToSmtp, waitForGreeting, sendSmtpCommand, closeSmtpConnection, generateRandomEmail } from '../../helpers/utils.js';
|
|
|
|
let testServer: ITestServer;
|
|
|
|
tap.test('setup - start SMTP server with large size limit', async () => {
|
|
testServer = await startTestServer({
|
|
port: 2532,
|
|
hostname: 'localhost',
|
|
size: 100 * 1024 * 1024 // 100MB limit for testing
|
|
});
|
|
expect(testServer).toBeInstanceOf(Object);
|
|
});
|
|
|
|
tap.test('EDGE-01: Very Large Email - test size limits and handling', async () => {
|
|
const testCases = [
|
|
{ size: 1 * 1024 * 1024, label: '1MB', shouldPass: true },
|
|
{ size: 10 * 1024 * 1024, label: '10MB', shouldPass: true },
|
|
{ size: 50 * 1024 * 1024, label: '50MB', shouldPass: true },
|
|
{ size: 101 * 1024 * 1024, label: '101MB', shouldPass: false } // Over limit
|
|
];
|
|
|
|
for (const testCase of testCases) {
|
|
console.log(`\n📧 Testing ${testCase.label} email...`);
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Check SIZE extension
|
|
await sendSmtpCommand(socket, `MAIL FROM:<large@example.com> SIZE=${testCase.size}`,
|
|
testCase.shouldPass ? '250' : '552');
|
|
|
|
if (testCase.shouldPass) {
|
|
// Continue with transaction
|
|
await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
|
|
await sendSmtpCommand(socket, 'DATA', '354');
|
|
|
|
// Send large content in chunks
|
|
const chunkSize = 65536; // 64KB chunks
|
|
const totalChunks = Math.ceil(testCase.size / chunkSize);
|
|
|
|
console.log(` Sending ${totalChunks} chunks...`);
|
|
|
|
// Headers
|
|
socket.write('From: large@example.com\r\n');
|
|
socket.write('To: recipient@example.com\r\n');
|
|
socket.write(`Subject: ${testCase.label} Test Email\r\n`);
|
|
socket.write('Content-Type: text/plain\r\n');
|
|
socket.write('\r\n');
|
|
|
|
// Body in chunks
|
|
let bytesSent = 100; // Approximate header size
|
|
const startTime = Date.now();
|
|
|
|
for (let i = 0; i < totalChunks; i++) {
|
|
const chunk = generateRandomEmail(Math.min(chunkSize, testCase.size - bytesSent));
|
|
socket.write(chunk);
|
|
bytesSent += chunk.length;
|
|
|
|
// Progress indicator every 10%
|
|
if (i % Math.floor(totalChunks / 10) === 0) {
|
|
const progress = (i / totalChunks * 100).toFixed(0);
|
|
console.log(` Progress: ${progress}%`);
|
|
}
|
|
|
|
// Small delay to avoid overwhelming
|
|
if (i % 100 === 0) {
|
|
await new Promise(resolve => setTimeout(resolve, 10));
|
|
}
|
|
}
|
|
|
|
// End of data
|
|
socket.write('\r\n.\r\n');
|
|
|
|
// Wait for response with longer timeout for large emails
|
|
const response = await new Promise<string>((resolve, reject) => {
|
|
let buffer = '';
|
|
const timeout = setTimeout(() => reject(new Error('Timeout')), 60000);
|
|
|
|
const onData = (data: Buffer) => {
|
|
buffer += data.toString();
|
|
if (buffer.includes('250') || buffer.includes('5')) {
|
|
clearTimeout(timeout);
|
|
socket.removeListener('data', onData);
|
|
resolve(buffer);
|
|
}
|
|
};
|
|
|
|
socket.on('data', onData);
|
|
});
|
|
|
|
const duration = Date.now() - startTime;
|
|
const throughputMBps = (testCase.size / 1024 / 1024) / (duration / 1000);
|
|
|
|
expect(response).toInclude('250');
|
|
console.log(` ✅ ${testCase.label} email accepted in ${duration}ms`);
|
|
console.log(` Throughput: ${throughputMBps.toFixed(2)} MB/s`);
|
|
|
|
} else {
|
|
console.log(` ✅ ${testCase.label} email properly rejected (over size limit)`);
|
|
}
|
|
|
|
} catch (error) {
|
|
if (!testCase.shouldPass && error.message.includes('552')) {
|
|
console.log(` ✅ ${testCase.label} email properly rejected: ${error.message}`);
|
|
} else {
|
|
throw error;
|
|
}
|
|
} finally {
|
|
await closeSmtpConnection(socket).catch(() => {});
|
|
}
|
|
}
|
|
});
|
|
|
|
tap.test('EDGE-01: Email size enforcement - SIZE parameter', async () => {
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
const ehloResponse = await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
|
|
// Extract SIZE limit from capabilities
|
|
const sizeMatch = ehloResponse.match(/250[- ]SIZE (\d+)/);
|
|
const sizeLimit = sizeMatch ? parseInt(sizeMatch[1]) : 0;
|
|
|
|
console.log(`📏 Server advertises SIZE limit: ${sizeLimit} bytes`);
|
|
expect(sizeLimit).toBeGreaterThan(0);
|
|
|
|
// Test SIZE parameter enforcement
|
|
const testSizes = [
|
|
{ size: 1000, shouldPass: true },
|
|
{ size: sizeLimit - 1000, shouldPass: true },
|
|
{ size: sizeLimit + 1000, shouldPass: false }
|
|
];
|
|
|
|
for (const test of testSizes) {
|
|
try {
|
|
const response = await sendSmtpCommand(
|
|
socket,
|
|
`MAIL FROM:<test@example.com> SIZE=${test.size}`
|
|
);
|
|
|
|
if (test.shouldPass) {
|
|
expect(response).toInclude('250');
|
|
console.log(` ✅ SIZE=${test.size} accepted`);
|
|
await sendSmtpCommand(socket, 'RSET', '250');
|
|
} else {
|
|
expect(response).toInclude('552');
|
|
console.log(` ✅ SIZE=${test.size} rejected`);
|
|
}
|
|
} catch (error) {
|
|
if (!test.shouldPass) {
|
|
console.log(` ✅ SIZE=${test.size} rejected: ${error.message}`);
|
|
} else {
|
|
throw error;
|
|
}
|
|
}
|
|
}
|
|
|
|
} finally {
|
|
await closeSmtpConnection(socket);
|
|
}
|
|
});
|
|
|
|
tap.test('EDGE-01: Memory efficiency with large emails', async () => {
|
|
// Get initial memory usage
|
|
const initialMemory = process.memoryUsage();
|
|
console.log('📊 Initial memory usage:', {
|
|
heapUsed: `${(initialMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
|
rss: `${(initialMemory.rss / 1024 / 1024).toFixed(2)} MB`
|
|
});
|
|
|
|
// Send a moderately large email
|
|
const socket = await connectToSmtp(testServer.hostname, testServer.port);
|
|
|
|
try {
|
|
await waitForGreeting(socket);
|
|
await sendSmtpCommand(socket, 'EHLO test.example.com', '250');
|
|
await sendSmtpCommand(socket, 'MAIL FROM:<memory@test.com>', '250');
|
|
await sendSmtpCommand(socket, 'RCPT TO:<recipient@example.com>', '250');
|
|
await sendSmtpCommand(socket, 'DATA', '354');
|
|
|
|
// Send 20MB email
|
|
const size = 20 * 1024 * 1024;
|
|
const chunkSize = 1024 * 1024; // 1MB chunks
|
|
|
|
socket.write('From: memory@test.com\r\n');
|
|
socket.write('To: recipient@example.com\r\n');
|
|
socket.write('Subject: Memory Test\r\n\r\n');
|
|
|
|
for (let i = 0; i < size / chunkSize; i++) {
|
|
socket.write(generateRandomEmail(chunkSize));
|
|
// Force garbage collection if available
|
|
if (global.gc) {
|
|
global.gc();
|
|
}
|
|
}
|
|
|
|
socket.write('\r\n.\r\n');
|
|
|
|
// Wait for response
|
|
await new Promise<void>((resolve) => {
|
|
const onData = (data: Buffer) => {
|
|
if (data.toString().includes('250')) {
|
|
socket.removeListener('data', onData);
|
|
resolve();
|
|
}
|
|
};
|
|
socket.on('data', onData);
|
|
});
|
|
|
|
// Check memory after processing
|
|
const finalMemory = process.memoryUsage();
|
|
const memoryIncrease = (finalMemory.heapUsed - initialMemory.heapUsed) / 1024 / 1024;
|
|
|
|
console.log('📊 Final memory usage:', {
|
|
heapUsed: `${(finalMemory.heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
|
rss: `${(finalMemory.rss / 1024 / 1024).toFixed(2)} MB`,
|
|
increase: `${memoryIncrease.toFixed(2)} MB`
|
|
});
|
|
|
|
// Memory increase should be reasonable (not storing entire email in memory)
|
|
expect(memoryIncrease).toBeLessThan(50); // Less than 50MB increase for 20MB email
|
|
console.log('✅ Memory efficiency test passed');
|
|
|
|
} finally {
|
|
await closeSmtpConnection(socket);
|
|
}
|
|
});
|
|
|
|
tap.test('cleanup - stop SMTP server', async () => {
|
|
await stopTestServer(testServer);
|
|
console.log('✅ Test server stopped');
|
|
});
|
|
|
|
export default tap.start(); |