458 lines
13 KiB
TypeScript
458 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-01: Basic reconnection after disconnect', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
maxReconnectAttempts: 3,
|
|
reconnectDelay: 1000,
|
|
debug: true
|
|
});
|
|
|
|
// First connection
|
|
await smtpClient.connect();
|
|
expect(smtpClient.isConnected()).toBeTruthy();
|
|
|
|
console.log('Initial connection established');
|
|
|
|
// Force disconnect
|
|
await smtpClient.close();
|
|
expect(smtpClient.isConnected()).toBeFalsy();
|
|
|
|
console.log('Connection closed');
|
|
|
|
// Reconnect
|
|
await smtpClient.connect();
|
|
expect(smtpClient.isConnected()).toBeTruthy();
|
|
|
|
console.log('Reconnection successful');
|
|
|
|
// Verify connection works
|
|
const result = await smtpClient.verify();
|
|
expect(result).toBeTruthy();
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CREL-01: Automatic reconnection on connection loss', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
enableAutoReconnect: true,
|
|
maxReconnectAttempts: 3,
|
|
reconnectDelay: 500,
|
|
debug: true
|
|
});
|
|
|
|
let reconnectCount = 0;
|
|
let connectionLostCount = 0;
|
|
|
|
smtpClient.on('error', (error) => {
|
|
console.log('Connection error:', error.message);
|
|
connectionLostCount++;
|
|
});
|
|
|
|
smtpClient.on('reconnecting', (attempt) => {
|
|
console.log(`Reconnection attempt ${attempt}`);
|
|
reconnectCount++;
|
|
});
|
|
|
|
smtpClient.on('reconnected', () => {
|
|
console.log('Successfully reconnected');
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Simulate connection loss by creating network interruption
|
|
const connectionInfo = smtpClient.getConnectionInfo();
|
|
if (connectionInfo && connectionInfo.socket) {
|
|
// Force close the socket
|
|
(connectionInfo.socket as net.Socket).destroy();
|
|
console.log('Simulated connection loss');
|
|
}
|
|
|
|
// Wait for automatic reconnection
|
|
await new Promise(resolve => setTimeout(resolve, 2000));
|
|
|
|
// Check if reconnection happened
|
|
if (smtpClient.isConnected()) {
|
|
console.log(`Automatic reconnection successful after ${reconnectCount} attempts`);
|
|
expect(reconnectCount).toBeGreaterThan(0);
|
|
} else {
|
|
console.log('Automatic reconnection not implemented or failed');
|
|
}
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CREL-01: Reconnection with exponential backoff', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
enableAutoReconnect: true,
|
|
maxReconnectAttempts: 5,
|
|
reconnectDelay: 100,
|
|
reconnectBackoffMultiplier: 2,
|
|
maxReconnectDelay: 5000,
|
|
debug: true
|
|
});
|
|
|
|
const reconnectDelays: number[] = [];
|
|
let lastReconnectTime = Date.now();
|
|
|
|
smtpClient.on('reconnecting', (attempt) => {
|
|
const now = Date.now();
|
|
const delay = now - lastReconnectTime;
|
|
reconnectDelays.push(delay);
|
|
lastReconnectTime = now;
|
|
console.log(`Reconnect attempt ${attempt} after ${delay}ms`);
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Temporarily make server unreachable
|
|
const originalPort = testServer.port;
|
|
testServer.port = 55555; // Non-existent port
|
|
|
|
// Trigger reconnection attempts
|
|
await smtpClient.close();
|
|
|
|
try {
|
|
await smtpClient.connect();
|
|
} catch (error) {
|
|
console.log('Expected connection failure:', error.message);
|
|
}
|
|
|
|
// Restore correct port
|
|
testServer.port = originalPort;
|
|
|
|
// Analyze backoff pattern
|
|
console.log('\nReconnection delays:', reconnectDelays);
|
|
|
|
// Check if delays increase (exponential backoff)
|
|
for (let i = 1; i < reconnectDelays.length; i++) {
|
|
const expectedIncrease = reconnectDelays[i] > reconnectDelays[i-1];
|
|
console.log(`Delay ${i}: ${reconnectDelays[i]}ms (${expectedIncrease ? 'increased' : 'did not increase'})`);
|
|
}
|
|
});
|
|
|
|
tap.test('CREL-01: Reconnection during email sending', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
enableAutoReconnect: true,
|
|
maxReconnectAttempts: 3,
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Reconnection Test',
|
|
text: 'Testing reconnection during send'
|
|
});
|
|
|
|
// Start sending email
|
|
let sendPromise = smtpClient.sendMail(email);
|
|
|
|
// Simulate brief connection loss during send
|
|
setTimeout(() => {
|
|
const connectionInfo = smtpClient.getConnectionInfo();
|
|
if (connectionInfo && connectionInfo.socket) {
|
|
console.log('Interrupting connection during send...');
|
|
(connectionInfo.socket as net.Socket).destroy();
|
|
}
|
|
}, 100);
|
|
|
|
try {
|
|
const result = await sendPromise;
|
|
console.log('Email sent successfully despite interruption:', result);
|
|
} catch (error) {
|
|
console.log('Send failed due to connection loss:', error.message);
|
|
|
|
// Try again after reconnection
|
|
if (smtpClient.isConnected() || await smtpClient.connect()) {
|
|
console.log('Retrying send after reconnection...');
|
|
const retryResult = await smtpClient.sendMail(email);
|
|
expect(retryResult).toBeTruthy();
|
|
console.log('Retry successful');
|
|
}
|
|
}
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CREL-01: Connection pool reconnection', async () => {
|
|
const pooledClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
pool: true,
|
|
maxConnections: 3,
|
|
maxMessages: 10,
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
// Monitor pool events
|
|
let poolErrors = 0;
|
|
let poolReconnects = 0;
|
|
|
|
pooledClient.on('pool-error', (error) => {
|
|
poolErrors++;
|
|
console.log('Pool error:', error.message);
|
|
});
|
|
|
|
pooledClient.on('pool-reconnect', (connectionId) => {
|
|
poolReconnects++;
|
|
console.log(`Pool connection ${connectionId} reconnected`);
|
|
});
|
|
|
|
await pooledClient.connect();
|
|
|
|
// Send multiple emails concurrently
|
|
const emails = Array.from({ length: 5 }, (_, i) => new Email({
|
|
from: 'sender@example.com',
|
|
to: [`recipient${i}@example.com`],
|
|
subject: `Pool Test ${i}`,
|
|
text: 'Testing connection pool'
|
|
}));
|
|
|
|
const sendPromises = emails.map(email => pooledClient.sendMail(email));
|
|
|
|
// Simulate connection issues during sending
|
|
setTimeout(() => {
|
|
console.log('Simulating pool connection issues...');
|
|
// In real scenario, pool connections might drop
|
|
}, 200);
|
|
|
|
const results = await Promise.allSettled(sendPromises);
|
|
|
|
const successful = results.filter(r => r.status === 'fulfilled').length;
|
|
const failed = results.filter(r => r.status === 'rejected').length;
|
|
|
|
console.log(`\nPool results: ${successful} successful, ${failed} failed`);
|
|
console.log(`Pool errors: ${poolErrors}, Pool reconnects: ${poolReconnects}`);
|
|
|
|
await pooledClient.close();
|
|
});
|
|
|
|
tap.test('CREL-01: Reconnection state preservation', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
auth: {
|
|
user: 'testuser',
|
|
pass: 'testpass'
|
|
},
|
|
connectionTimeout: 5000,
|
|
debug: true
|
|
});
|
|
|
|
// Track state
|
|
let wasAuthenticated = false;
|
|
let capabilities: string[] = [];
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Get initial state
|
|
const ehloResponse = await smtpClient.sendCommand('EHLO testclient');
|
|
capabilities = ehloResponse.split('\n').filter(line => line.startsWith('250-'));
|
|
console.log(`Initial capabilities: ${capabilities.length}`);
|
|
|
|
// Try authentication
|
|
try {
|
|
await smtpClient.sendCommand('AUTH PLAIN ' + Buffer.from('\0testuser\0testpass').toString('base64'));
|
|
wasAuthenticated = true;
|
|
} catch (error) {
|
|
console.log('Auth not supported or failed');
|
|
}
|
|
|
|
// Force reconnection
|
|
await smtpClient.close();
|
|
await smtpClient.connect();
|
|
|
|
// Check if state is preserved
|
|
const newEhloResponse = await smtpClient.sendCommand('EHLO testclient');
|
|
const newCapabilities = newEhloResponse.split('\n').filter(line => line.startsWith('250-'));
|
|
|
|
console.log(`\nState after reconnection:`);
|
|
console.log(` Capabilities preserved: ${newCapabilities.length === capabilities.length}`);
|
|
console.log(` Auth state: ${wasAuthenticated ? 'Should re-authenticate' : 'No auth needed'}`);
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CREL-01: Maximum reconnection attempts', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: 'non.existent.host',
|
|
port: 25,
|
|
secure: false,
|
|
connectionTimeout: 1000,
|
|
enableAutoReconnect: true,
|
|
maxReconnectAttempts: 3,
|
|
reconnectDelay: 100,
|
|
debug: true
|
|
});
|
|
|
|
let attemptCount = 0;
|
|
let finalError: Error | null = null;
|
|
|
|
smtpClient.on('reconnecting', (attempt) => {
|
|
attemptCount = attempt;
|
|
console.log(`Reconnection attempt ${attempt}/3`);
|
|
});
|
|
|
|
smtpClient.on('max-reconnect-attempts', () => {
|
|
console.log('Maximum reconnection attempts reached');
|
|
});
|
|
|
|
try {
|
|
await smtpClient.connect();
|
|
} catch (error) {
|
|
finalError = error;
|
|
console.log('Final error after all attempts:', error.message);
|
|
}
|
|
|
|
expect(finalError).toBeTruthy();
|
|
expect(attemptCount).toBeLessThanOrEqual(3);
|
|
|
|
console.log(`\nTotal attempts made: ${attemptCount}`);
|
|
});
|
|
|
|
tap.test('CREL-01: Reconnection with different endpoints', async () => {
|
|
// Test failover to backup servers
|
|
const endpoints = [
|
|
{ host: 'primary.invalid', port: 25 },
|
|
{ host: 'secondary.invalid', port: 25 },
|
|
{ host: testServer.hostname, port: testServer.port } // Working server
|
|
];
|
|
|
|
let currentEndpoint = 0;
|
|
|
|
const smtpClient = createSmtpClient({
|
|
host: endpoints[currentEndpoint].host,
|
|
port: endpoints[currentEndpoint].port,
|
|
secure: false,
|
|
connectionTimeout: 1000,
|
|
debug: true
|
|
});
|
|
|
|
smtpClient.on('connection-failed', () => {
|
|
console.log(`Failed to connect to ${endpoints[currentEndpoint].host}`);
|
|
currentEndpoint++;
|
|
|
|
if (currentEndpoint < endpoints.length) {
|
|
console.log(`Trying next endpoint: ${endpoints[currentEndpoint].host}`);
|
|
smtpClient.updateOptions({
|
|
host: endpoints[currentEndpoint].host,
|
|
port: endpoints[currentEndpoint].port
|
|
});
|
|
}
|
|
});
|
|
|
|
// Try connecting with failover
|
|
let connected = false;
|
|
for (let i = 0; i < endpoints.length && !connected; i++) {
|
|
try {
|
|
if (i > 0) {
|
|
smtpClient.updateOptions({
|
|
host: endpoints[i].host,
|
|
port: endpoints[i].port
|
|
});
|
|
}
|
|
await smtpClient.connect();
|
|
connected = true;
|
|
console.log(`Successfully connected to endpoint ${i + 1}: ${endpoints[i].host}`);
|
|
} catch (error) {
|
|
console.log(`Endpoint ${i + 1} failed: ${error.message}`);
|
|
}
|
|
}
|
|
|
|
expect(connected).toBeTruthy();
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('CREL-01: Graceful degradation', async () => {
|
|
const smtpClient = createSmtpClient({
|
|
host: testServer.hostname,
|
|
port: testServer.port,
|
|
secure: false,
|
|
connectionTimeout: 5000,
|
|
features: {
|
|
pipelining: true,
|
|
enhancedStatusCodes: true,
|
|
'8bitmime': true
|
|
},
|
|
debug: true
|
|
});
|
|
|
|
await smtpClient.connect();
|
|
|
|
// Test feature availability
|
|
const ehloResponse = await smtpClient.sendCommand('EHLO testclient');
|
|
|
|
console.log('\nChecking feature support after reconnection:');
|
|
const features = ['PIPELINING', 'ENHANCEDSTATUSCODES', '8BITMIME', 'STARTTLS'];
|
|
|
|
for (const feature of features) {
|
|
const supported = ehloResponse.includes(feature);
|
|
console.log(` ${feature}: ${supported ? 'Supported' : 'Not supported'}`);
|
|
|
|
if (!supported && smtpClient.hasFeature && smtpClient.hasFeature(feature)) {
|
|
console.log(` -> Disabling ${feature} for graceful degradation`);
|
|
}
|
|
}
|
|
|
|
// Simulate reconnection to less capable server
|
|
await smtpClient.close();
|
|
|
|
console.log('\nSimulating reconnection to server with fewer features...');
|
|
await smtpClient.connect();
|
|
|
|
// Should still be able to send basic emails
|
|
const email = new Email({
|
|
from: 'sender@example.com',
|
|
to: ['recipient@example.com'],
|
|
subject: 'Graceful Degradation Test',
|
|
text: 'Basic email functionality still works'
|
|
});
|
|
|
|
const result = await smtpClient.sendMail(email);
|
|
expect(result).toBeTruthy();
|
|
|
|
console.log('Basic email sent successfully with degraded features');
|
|
|
|
await smtpClient.close();
|
|
});
|
|
|
|
tap.test('cleanup test SMTP server', async () => {
|
|
if (testServer) {
|
|
await testServer.stop();
|
|
}
|
|
});
|
|
|
|
export default tap.start(); |