dcrouter/test/suite/smtpclient_edge-cases/test.cedge-04.resource-constraints.ts

488 lines
17 KiB
TypeScript
Raw Normal View History

2025-05-24 17:00:59 +00:00
import { expect, tap } from '@git.zone/tstest/tapbundle';
import * as plugins from './plugins.js';
import { createTestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
tap.test('CEDGE-04: should handle resource constraints gracefully', async (tools) => {
const testId = 'CEDGE-04-resource-constraints';
console.log(`\n${testId}: Testing resource constraint handling...`);
let scenarioCount = 0;
// Scenario 1: Very slow server responses
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing very slow server responses`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
// Slow greeting
setTimeout(() => {
socket.write('220 mail.example.com ESMTP\r\n');
}, 2000);
socket.on('data', async (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
// Add delays to all responses
const delay = (ms: number) => new Promise(resolve => setTimeout(resolve, ms));
if (command.startsWith('EHLO')) {
await delay(1500);
socket.write('250-mail.example.com\r\n');
await delay(500);
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
await delay(2000);
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
await delay(1000);
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
await delay(1500);
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
await delay(3000);
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
await delay(500);
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000, // 10 second timeout
greetingTimeout: 5000,
socketTimeout: 10000
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Slow server test',
text: 'Testing slow server responses'
});
console.log(' Sending email (this will take time due to delays)...');
const start = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - start;
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'} (took ${elapsed}ms)`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
// Scenario 2: Server with limited buffer (sends data in small chunks)
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing server sending data in small chunks`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
// Send greeting in small chunks
const greeting = '220 mail.example.com ESMTP\r\n';
for (let i = 0; i < greeting.length; i += 5) {
socket.write(greeting.slice(i, i + 5));
await new Promise(resolve => setTimeout(resolve, 50));
}
socket.on('data', async (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
// Send capabilities in very small chunks
const response = '250-mail.example.com\r\n250-SIZE 10485760\r\n250-8BITMIME\r\n250 OK\r\n';
for (let i = 0; i < response.length; i += 3) {
socket.write(response.slice(i, i + 3));
await new Promise(resolve => setTimeout(resolve, 20));
}
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Chunked response test',
text: 'Testing fragmented server responses'
});
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
// Scenario 3: Server with connection limit
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing server connection limits`);
let connectionCount = 0;
const maxConnections = 2;
const testServer = await createTestServer({
onConnection: async (socket) => {
connectionCount++;
console.log(` [Server] Connection ${connectionCount} (max: ${maxConnections})`);
if (connectionCount > maxConnections) {
socket.write('421 4.3.2 Too many connections, try again later\r\n');
socket.end();
return;
}
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('close', () => {
connectionCount--;
console.log(` [Server] Connection closed, count: ${connectionCount}`);
});
socket.on('data', (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
if (command.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
// Try to send multiple emails concurrently
const emails = Array(3).fill(null).map((_, i) =>
new plugins.smartmail.Email({
from: 'sender@example.com',
to: [`recipient${i + 1}@example.com`],
subject: `Connection limit test ${i + 1}`,
text: `Testing connection limits - email ${i + 1}`
})
);
const results = await Promise.allSettled(
emails.map(async (email, i) => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
console.log(` Sending email ${i + 1}...`);
try {
const result = await client.sendMail(email);
return { index: i + 1, success: true, result };
} catch (error) {
return { index: i + 1, success: false, error: error.message };
}
})
);
results.forEach((result, i) => {
if (result.status === 'fulfilled') {
const { index, success, error } = result.value;
console.log(` Email ${index}: ${success ? 'Success' : `Failed - ${error}`}`);
}
});
// At least some should succeed
const successes = results.filter(r =>
r.status === 'fulfilled' && r.value.success
);
expect(successes.length).toBeGreaterThan(0);
await testServer.server.close();
})();
// Scenario 4: Server with memory constraints (limited line length)
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing server line length limits`);
const maxLineLength = 100;
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
let buffer = '';
socket.on('data', (data) => {
buffer += data.toString();
let lines = buffer.split('\r\n');
buffer = lines.pop() || ''; // Keep incomplete line in buffer
lines.forEach(line => {
if (line.length === 0) return;
console.log(` [Server] Received line (${line.length} chars): ${line.substring(0, 50)}...`);
if (line.length > maxLineLength && !line.startsWith('DATA')) {
socket.write(`500 5.5.2 Line too long (max ${maxLineLength})\r\n`);
return;
}
if (line.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write(`250-SIZE ${maxLineLength}\r\n`);
socket.write('250 OK\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (line === '.') {
socket.write('250 OK\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
}
});
// Test with normal email first
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Line length test',
text: 'Testing server line length limits with a reasonably short message that should work fine.'
});
const result = await smtpClient.sendMail(email);
console.log(` Normal email result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
// Test with very long subject
const longEmail = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'A'.repeat(150), // Very long subject
text: 'Short body'
});
try {
const longResult = await smtpClient.sendMail(longEmail);
console.log(` Long subject email: ${longResult.messageId ? 'Success (folded properly)' : 'Failed'}`);
} catch (error) {
console.log(` Long subject email failed as expected: ${error.message}`);
}
await testServer.server.close();
})();
// Scenario 5: Server with CPU constraints (slow command processing)
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing server with slow command processing`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
socket.on('data', async (data) => {
const command = data.toString().trim();
console.log(` [Server] Received: ${command}`);
// Simulate CPU-intensive processing
const busyWait = (ms: number) => {
const start = Date.now();
while (Date.now() - start < ms) {
// Busy wait
}
};
if (command.startsWith('EHLO')) {
busyWait(500);
socket.write('250-mail.example.com\r\n');
busyWait(200);
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
busyWait(300);
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
busyWait(400);
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
busyWait(200);
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
busyWait(1000); // Slow processing of message
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
socketTimeout: 10000 // Higher timeout for slow server
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'CPU constraint test',
text: 'Testing server with slow processing'
});
console.log(' Sending email to slow server...');
const start = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - start;
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'} (took ${elapsed}ms)`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
expect(elapsed).toBeGreaterThan(2000); // Should take at least 2 seconds
await testServer.server.close();
})();
// Scenario 6: Server with limited command buffer
await (async () => {
scenarioCount++;
console.log(`\nScenario ${scenarioCount}: Testing server with limited command buffer`);
const testServer = await createTestServer({
onConnection: async (socket) => {
console.log(' [Server] Client connected');
socket.write('220 mail.example.com ESMTP\r\n');
const commandQueue: string[] = [];
let processing = false;
const processCommands = async () => {
if (processing || commandQueue.length === 0) return;
processing = true;
while (commandQueue.length > 0) {
const command = commandQueue.shift()!;
console.log(` [Server] Processing: ${command}`);
// Simulate slow processing
await new Promise(resolve => setTimeout(resolve, 100));
if (command.startsWith('EHLO')) {
socket.write('250-mail.example.com\r\n');
socket.write('250-PIPELINING\r\n'); // Support pipelining
socket.write('250 OK\r\n');
} else if (command.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (command.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (command === 'DATA') {
socket.write('354 Start mail input\r\n');
} else if (command === '.') {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
}
processing = false;
};
socket.on('data', (data) => {
const commands = data.toString().split('\r\n').filter(cmd => cmd.length > 0);
commands.forEach(cmd => {
if (commandQueue.length >= 5) {
console.log(' [Server] Command buffer full, rejecting command');
socket.write('421 4.3.2 Command buffer full\r\n');
socket.end();
return;
}
commandQueue.push(cmd);
});
processCommands();
});
}
});
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false
});
const email = new plugins.smartmail.Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Command buffer test',
text: 'Testing limited command buffer'
});
const result = await smtpClient.sendMail(email);
console.log(` Result: ${result.messageId ? 'Success' : 'Failed'}`);
expect(result).toBeDefined();
expect(result.messageId).toBeDefined();
await testServer.server.close();
})();
console.log(`\n${testId}: All ${scenarioCount} resource constraint scenarios tested ✓`);
});