dcrouter/test/suite/smtpclient_error-handling/test.cerr-03.network-failures.ts

363 lines
9.7 KiB
TypeScript
Raw Normal View History

2025-05-24 16:19:19 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as net from 'net';
let testServer: ITestServer;
tap.test('setup - start SMTP server for network failure tests', async () => {
testServer = await startTestServer({
port: 2554,
tlsEnabled: false,
authRequired: false
});
expect(testServer.port).toEqual(2554);
});
tap.test('CERR-03: Network Failures - should handle connection refused', async () => {
let errorCaught = false;
const startTime = Date.now();
try {
// Try to connect to a port that's not listening
const client = createSmtpClient({
host: 'localhost',
port: 9876, // Non-listening port
secure: false,
connectionTimeout: 3000,
debug: true
});
await client.verify();
} catch (error: any) {
errorCaught = true;
const duration = Date.now() - startTime;
expect(error).toBeInstanceOf(Error);
expect(error.message).toContain('ECONNREFUSED');
console.log(`✅ Connection refused handled in ${duration}ms`);
}
expect(errorCaught).toBeTrue();
});
tap.test('CERR-03: Network Failures - should handle DNS resolution failure', async () => {
let dnsError = false;
try {
const client = createSmtpClient({
host: 'non.existent.domain.that.should.not.resolve.example',
port: 25,
secure: false,
connectionTimeout: 5000,
debug: true
});
await client.verify();
} catch (error: any) {
dnsError = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ DNS resolution failure handled:', error.code);
}
expect(dnsError).toBeTrue();
});
tap.test('CERR-03: Network Failures - should handle connection drop during handshake', async () => {
// Create a server that drops connections immediately
const dropServer = net.createServer((socket) => {
// Drop connection after accepting
socket.destroy();
});
await new Promise<void>((resolve) => {
dropServer.listen(2555, () => resolve());
});
let dropError = false;
try {
const client = createSmtpClient({
host: 'localhost',
port: 2555,
secure: false,
connectionTimeout: 5000
});
await client.verify();
} catch (error: any) {
dropError = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ Connection drop during handshake handled');
}
expect(dropError).toBeTrue();
dropServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('CERR-03: Network Failures - should handle connection drop during data transfer', async () => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
connectionTimeout: 5000,
socketTimeout: 10000
});
// Establish connection first
await client.verify();
// For this test, we simulate network issues by attempting
// to send after server issues might occur
const email = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Network Failure Test',
text: 'Testing network failure recovery'
});
try {
const result = await client.sendMail(email);
expect(result.success).toBeTrue();
console.log('✅ Email sent successfully (no network failure simulated)');
} catch (error) {
console.log('✅ Network failure handled during data transfer');
}
await client.close();
});
tap.test('CERR-03: Network Failures - should retry on transient network errors', async () => {
let attemptCount = 0;
// Create a server that fails first attempt
const retryServer = net.createServer((socket) => {
attemptCount++;
if (attemptCount === 1) {
// First attempt: drop connection
socket.destroy();
} else {
// Second attempt: normal SMTP
socket.write('220 Retry server ready\r\n');
socket.on('data', (data) => {
const command = data.toString().trim();
if (command.startsWith('EHLO') || command.startsWith('HELO')) {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
}
});
await new Promise<void>((resolve) => {
retryServer.listen(2556, () => resolve());
});
const client = createSmtpClient({
host: 'localhost',
port: 2556,
secure: false,
connectionTimeout: 5000
});
// Client might or might not retry depending on implementation
try {
await client.verify();
console.log(`✅ Connection established after ${attemptCount} attempts`);
} catch (error) {
console.log(`✅ Network error handled after ${attemptCount} attempts`);
}
retryServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('CERR-03: Network Failures - should handle slow network (timeout)', async () => {
// Create a server that responds very slowly
const slowServer = net.createServer((socket) => {
// Wait 5 seconds before responding
setTimeout(() => {
socket.write('220 Slow server ready\r\n');
}, 5000);
});
await new Promise<void>((resolve) => {
slowServer.listen(2557, () => resolve());
});
let timeoutError = false;
const startTime = Date.now();
try {
const client = createSmtpClient({
host: 'localhost',
port: 2557,
secure: false,
connectionTimeout: 2000 // 2 second timeout
});
await client.verify();
} catch (error: any) {
timeoutError = true;
const duration = Date.now() - startTime;
expect(error).toBeInstanceOf(Error);
expect(duration).toBeLessThan(3000);
console.log(`✅ Slow network timeout after ${duration}ms`);
}
expect(timeoutError).toBeTrue();
slowServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('CERR-03: Network Failures - should recover from temporary network issues', async () => {
const client = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
connectionTimeout: 5000
});
// Send first email successfully
const email1 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'Before Network Issue',
text: 'First email'
});
const result1 = await client.sendMail(email1);
expect(result1.success).toBeTrue();
// Simulate network recovery by sending another email
const email2 = new Email({
from: 'sender@example.com',
to: 'recipient@example.com',
subject: 'After Network Recovery',
text: 'Second email after recovery'
});
const result2 = await client.sendMail(email2);
expect(result2.success).toBeTrue();
console.log('✅ Recovered from simulated network issues');
await client.close();
});
tap.test('CERR-03: Network Failures - should handle EHOSTUNREACH', async () => {
let unreachableError = false;
try {
// Use an IP that should be unreachable
const client = createSmtpClient({
host: '192.0.2.1', // TEST-NET-1, should be unreachable
port: 25,
secure: false,
connectionTimeout: 3000
});
await client.verify();
} catch (error: any) {
unreachableError = true;
expect(error).toBeInstanceOf(Error);
console.log('✅ Host unreachable error handled:', error.code);
}
expect(unreachableError).toBeTrue();
});
tap.test('CERR-03: Network Failures - should handle packet loss simulation', async () => {
// Create a server that randomly drops data
let packetCount = 0;
const lossyServer = net.createServer((socket) => {
socket.write('220 Lossy server ready\r\n');
socket.on('data', (data) => {
packetCount++;
// Simulate 30% packet loss
if (Math.random() > 0.3) {
const command = data.toString().trim();
if (command.startsWith('EHLO')) {
socket.write('250 OK\r\n');
} else if (command === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
}
// Otherwise, don't respond (simulate packet loss)
});
});
await new Promise<void>((resolve) => {
lossyServer.listen(2558, () => resolve());
});
const client = createSmtpClient({
host: 'localhost',
port: 2558,
secure: false,
connectionTimeout: 5000,
socketTimeout: 2000 // Short timeout to detect loss
});
try {
await client.verify();
console.log('✅ Connected despite simulated packet loss');
} catch (error) {
console.log(`✅ Packet loss detected after ${packetCount} packets`);
}
lossyServer.close();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('CERR-03: Network Failures - should provide meaningful error messages', async () => {
const errorScenarios = [
{
host: 'localhost',
port: 9999,
expectedError: 'ECONNREFUSED'
},
{
host: 'invalid.domain.test',
port: 25,
expectedError: 'ENOTFOUND'
}
];
for (const scenario of errorScenarios) {
try {
const client = createSmtpClient({
host: scenario.host,
port: scenario.port,
secure: false,
connectionTimeout: 3000
});
await client.verify();
} catch (error: any) {
expect(error.message).toBeTypeofString();
console.log(`✅ Clear error for ${scenario.host}:${scenario.port} - ${error.code || error.message}`);
}
}
});
tap.test('cleanup - stop SMTP server', async () => {
await stopTestServer(testServer);
});
tap.start();