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();