dcrouter/test/suite/smtpclient_reliability/test.crel-02.network-interruption.ts
2025-05-24 16:19:19 +00:00

459 lines
13 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestSmtpServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as net from 'net';
let testServer: any;
tap.test('setup test SMTP server', async () => {
testServer = await startTestSmtpServer();
expect(testServer).toBeTruthy();
expect(testServer.port).toBeGreaterThan(0);
});
tap.test('CREL-02: Handle sudden connection drop', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 10000,
debug: true
});
let connectionDropped = false;
let errorReceived = false;
smtpClient.on('error', (error) => {
errorReceived = true;
console.log('Error event received:', error.message);
});
smtpClient.on('close', () => {
connectionDropped = true;
console.log('Connection closed unexpectedly');
});
await smtpClient.connect();
// Get the underlying socket
const connectionInfo = smtpClient.getConnectionInfo();
const socket = connectionInfo?.socket as net.Socket;
if (socket) {
// Simulate sudden network drop
console.log('Simulating sudden network disconnection...');
socket.destroy();
// Wait for events to fire
await new Promise(resolve => setTimeout(resolve, 1000));
expect(connectionDropped || errorReceived).toBeTruthy();
expect(smtpClient.isConnected()).toBeFalsy();
}
console.log(`Connection dropped: ${connectionDropped}, Error received: ${errorReceived}`);
});
tap.test('CREL-02: Network timeout handling', async () => {
// Create a server that accepts connections but doesn't respond
const silentServer = net.createServer((socket) => {
console.log('Silent server: Client connected, not responding...');
// Don't send any data
});
await new Promise<void>((resolve) => {
silentServer.listen(0, '127.0.0.1', () => {
resolve();
});
});
const silentPort = (silentServer.address() as net.AddressInfo).port;
const smtpClient = createSmtpClient({
host: '127.0.0.1',
port: silentPort,
secure: false,
connectionTimeout: 2000, // 2 second timeout
debug: true
});
const startTime = Date.now();
let timeoutError = false;
try {
await smtpClient.connect();
} catch (error) {
const elapsed = Date.now() - startTime;
timeoutError = true;
console.log(`Connection timed out after ${elapsed}ms`);
console.log('Error:', error.message);
expect(elapsed).toBeGreaterThanOrEqual(1900); // Allow small margin
expect(elapsed).toBeLessThan(3000);
}
expect(timeoutError).toBeTruthy();
silentServer.close();
});
tap.test('CREL-02: Packet loss simulation', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
commandTimeout: 3000,
debug: true
});
await smtpClient.connect();
// Create a proxy that randomly drops packets
let packetDropRate = 0.3; // 30% packet loss
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
let droppedCommands = 0;
let totalCommands = 0;
smtpClient.sendCommand = async (command: string) => {
totalCommands++;
if (Math.random() < packetDropRate && !command.startsWith('QUIT')) {
droppedCommands++;
console.log(`Simulating packet loss for: ${command.trim()}`);
// Simulate timeout
return new Promise((_, reject) => {
setTimeout(() => reject(new Error('Command timeout')), 3000);
});
}
return originalSendCommand(command);
};
// Try to send email with simulated packet loss
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Packet Loss Test',
text: 'Testing reliability with packet loss'
});
let retries = 0;
let success = false;
const maxRetries = 3;
while (retries < maxRetries && !success) {
try {
console.log(`\nAttempt ${retries + 1}/${maxRetries}`);
const result = await smtpClient.sendMail(email);
success = true;
console.log('Email sent successfully despite packet loss');
} catch (error) {
retries++;
console.log(`Attempt failed: ${error.message}`);
if (retries < maxRetries) {
console.log('Retrying...');
// Reset connection for retry
if (!smtpClient.isConnected()) {
await smtpClient.connect();
}
}
}
}
console.log(`\nPacket loss simulation results:`);
console.log(` Total commands: ${totalCommands}`);
console.log(` Dropped: ${droppedCommands} (${(droppedCommands/totalCommands*100).toFixed(1)}%)`);
console.log(` Success after ${retries} retries: ${success}`);
await smtpClient.close();
});
tap.test('CREL-02: Bandwidth throttling', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
debug: true
});
await smtpClient.connect();
// Simulate bandwidth throttling by adding delays
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
const bytesPerSecond = 1024; // 1KB/s throttle
smtpClient.sendCommand = async (command: string) => {
const commandBytes = Buffer.byteLength(command, 'utf8');
const delay = (commandBytes / bytesPerSecond) * 1000;
console.log(`Throttling: ${commandBytes} bytes, ${delay.toFixed(0)}ms delay`);
await new Promise(resolve => setTimeout(resolve, delay));
return originalSendCommand(command);
};
// Send email with large content
const largeText = 'x'.repeat(10000); // 10KB of text
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Bandwidth Throttling Test',
text: largeText
});
console.log('\nSending large email with bandwidth throttling...');
const startTime = Date.now();
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
const effectiveSpeed = (largeText.length / elapsed) * 1000;
console.log(`Email sent in ${elapsed}ms`);
console.log(`Effective speed: ${effectiveSpeed.toFixed(0)} bytes/second`);
expect(result).toBeTruthy();
expect(elapsed).toBeGreaterThan(5000); // Should take several seconds
await smtpClient.close();
});
tap.test('CREL-02: Connection stability monitoring', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
keepAlive: true,
keepAliveInterval: 1000,
debug: true
});
// Track connection stability
const metrics = {
keepAlivesSent: 0,
keepAlivesSuccessful: 0,
errors: 0,
latencies: [] as number[]
};
smtpClient.on('keepalive', () => {
metrics.keepAlivesSent++;
});
await smtpClient.connect();
// Monitor connection for 10 seconds
console.log('Monitoring connection stability for 10 seconds...');
const monitoringDuration = 10000;
const checkInterval = 2000;
const endTime = Date.now() + monitoringDuration;
while (Date.now() < endTime) {
const startTime = Date.now();
try {
// Send NOOP to check connection
await smtpClient.sendCommand('NOOP');
const latency = Date.now() - startTime;
metrics.latencies.push(latency);
metrics.keepAlivesSuccessful++;
console.log(`Connection check OK, latency: ${latency}ms`);
} catch (error) {
metrics.errors++;
console.log(`Connection check failed: ${error.message}`);
}
await new Promise(resolve => setTimeout(resolve, checkInterval));
}
// Calculate stability metrics
const avgLatency = metrics.latencies.reduce((a, b) => a + b, 0) / metrics.latencies.length;
const maxLatency = Math.max(...metrics.latencies);
const minLatency = Math.min(...metrics.latencies);
const successRate = (metrics.keepAlivesSuccessful / (metrics.keepAlivesSuccessful + metrics.errors)) * 100;
console.log('\nConnection Stability Report:');
console.log(` Success rate: ${successRate.toFixed(1)}%`);
console.log(` Average latency: ${avgLatency.toFixed(1)}ms`);
console.log(` Min/Max latency: ${minLatency}ms / ${maxLatency}ms`);
console.log(` Errors: ${metrics.errors}`);
expect(successRate).toBeGreaterThan(90); // Expect high reliability
await smtpClient.close();
});
tap.test('CREL-02: Intermittent network issues', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Simulate intermittent network issues
let issueActive = false;
let issueCount = 0;
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
// Create intermittent issues every few seconds
const issueInterval = setInterval(() => {
issueActive = !issueActive;
if (issueActive) {
issueCount++;
console.log(`\nNetwork issue ${issueCount} started`);
} else {
console.log(`Network issue ${issueCount} resolved`);
}
}, 3000);
smtpClient.sendCommand = async (command: string) => {
if (issueActive && Math.random() > 0.5) {
console.log(`Command affected by network issue: ${command.trim()}`);
throw new Error('Network unreachable');
}
return originalSendCommand(command);
};
// Send multiple emails during intermittent issues
const emails = Array.from({ length: 5 }, (_, i) => new Email({
from: 'sender@example.com',
to: [`recipient${i}@example.com`],
subject: `Intermittent Network Test ${i}`,
text: 'Testing with intermittent network issues'
}));
const results = await Promise.allSettled(
emails.map(async (email, i) => {
// Add random delay to spread out sends
await new Promise(resolve => setTimeout(resolve, i * 1000));
return smtpClient.sendMail(email);
})
);
clearInterval(issueInterval);
const successful = results.filter(r => r.status === 'fulfilled').length;
const failed = results.filter(r => r.status === 'rejected').length;
console.log(`\nResults with intermittent issues:`);
console.log(` Successful: ${successful}/${emails.length}`);
console.log(` Failed: ${failed}/${emails.length}`);
console.log(` Network issues encountered: ${issueCount}`);
// Some should succeed despite issues
expect(successful).toBeGreaterThan(0);
await smtpClient.close();
});
tap.test('CREL-02: DNS resolution failures', async () => {
// Test handling of DNS resolution failures
const invalidHosts = [
'non.existent.domain.invalid',
'another.fake.domain.test',
'...',
'domain with spaces.com'
];
for (const host of invalidHosts) {
console.log(`\nTesting DNS resolution for: ${host}`);
const smtpClient = createSmtpClient({
host: host,
port: 25,
secure: false,
connectionTimeout: 3000,
dnsTimeout: 2000,
debug: true
});
const startTime = Date.now();
let errorType = '';
try {
await smtpClient.connect();
} catch (error) {
const elapsed = Date.now() - startTime;
errorType = error.code || 'unknown';
console.log(` Failed after ${elapsed}ms`);
console.log(` Error type: ${errorType}`);
console.log(` Error message: ${error.message}`);
}
expect(errorType).toBeTruthy();
}
});
tap.test('CREL-02: Network latency spikes', async () => {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 10000,
commandTimeout: 5000,
debug: true
});
await smtpClient.connect();
// Simulate latency spikes
const originalSendCommand = smtpClient.sendCommand.bind(smtpClient);
let spikeCount = 0;
smtpClient.sendCommand = async (command: string) => {
// Random latency spikes
if (Math.random() < 0.2) { // 20% chance of spike
spikeCount++;
const spikeDelay = 1000 + Math.random() * 3000; // 1-4 second spike
console.log(`Latency spike ${spikeCount}: ${spikeDelay.toFixed(0)}ms delay`);
await new Promise(resolve => setTimeout(resolve, spikeDelay));
}
return originalSendCommand(command);
};
// Send email with potential latency spikes
const email = new Email({
from: 'sender@example.com',
to: ['recipient@example.com'],
subject: 'Latency Spike Test',
text: 'Testing behavior during network latency spikes'
});
console.log('\nSending email with potential latency spikes...');
const startTime = Date.now();
try {
const result = await smtpClient.sendMail(email);
const elapsed = Date.now() - startTime;
console.log(`\nEmail sent successfully in ${elapsed}ms`);
console.log(`Latency spikes encountered: ${spikeCount}`);
expect(result).toBeTruthy();
if (spikeCount > 0) {
expect(elapsed).toBeGreaterThan(1000); // Should show impact of spikes
}
} catch (error) {
console.log('Send failed due to timeout:', error.message);
// This is acceptable if spike exceeded timeout
}
await smtpClient.close();
});
tap.test('cleanup test SMTP server', async () => {
if (testServer) {
await testServer.stop();
}
});
export default tap.start();