import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestSmtpServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../helpers/smtp.client.js'; import * as net from 'net'; let testServer: any; tap.test('setup test SMTP server', async () => { testServer = await startTestSmtpServer({ socketTimeout: 30000 // 30 second timeout for keep-alive tests }); expect(testServer).toBeTruthy(); expect(testServer.port).toBeGreaterThan(0); }); tap.test('CCM-11: Basic keep-alive functionality', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, keepAlive: true, keepAliveInterval: 5000, // 5 seconds connectionTimeout: 10000, debug: true }); // Connect to server const connected = await smtpClient.connect(); expect(connected).toBeTruthy(); expect(smtpClient.isConnected()).toBeTruthy(); // Track keep-alive activity let keepAliveCount = 0; let lastActivity = Date.now(); smtpClient.on('keepalive', () => { keepAliveCount++; const elapsed = Date.now() - lastActivity; console.log(`Keep-alive sent after ${elapsed}ms`); lastActivity = Date.now(); }); // Wait for multiple keep-alive cycles await new Promise(resolve => setTimeout(resolve, 12000)); // Wait 12 seconds // Should have sent at least 2 keep-alive messages expect(keepAliveCount).toBeGreaterThanOrEqual(2); // Connection should still be alive expect(smtpClient.isConnected()).toBeTruthy(); await smtpClient.close(); }); tap.test('CCM-11: Keep-alive with NOOP command', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, keepAlive: true, keepAliveInterval: 3000, connectionTimeout: 10000, debug: true }); await smtpClient.connect(); let noopResponses = 0; // Send NOOP commands manually to simulate keep-alive for (let i = 0; i < 3; i++) { await new Promise(resolve => setTimeout(resolve, 2000)); try { const response = await smtpClient.sendCommand('NOOP'); if (response && response.includes('250')) { noopResponses++; console.log(`NOOP response ${i + 1}: ${response.trim()}`); } } catch (error) { console.error('NOOP error:', error.message); } } expect(noopResponses).toEqual(3); expect(smtpClient.isConnected()).toBeTruthy(); await smtpClient.close(); }); tap.test('CCM-11: Connection idle timeout without keep-alive', async () => { // Create a client without keep-alive const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, keepAlive: false, // Disabled connectionTimeout: 5000, socketTimeout: 5000, // 5 second socket timeout debug: true }); await smtpClient.connect(); expect(smtpClient.isConnected()).toBeTruthy(); let disconnected = false; let timeoutError = false; smtpClient.on('timeout', () => { timeoutError = true; console.log('Socket timeout detected'); }); smtpClient.on('close', () => { disconnected = true; console.log('Connection closed'); }); smtpClient.on('error', (error: Error) => { console.log('Connection error:', error.message); if (error.message.includes('timeout')) { timeoutError = true; } }); // Wait for timeout (longer than socket timeout) await new Promise(resolve => setTimeout(resolve, 7000)); // Without keep-alive, connection might timeout // This depends on server configuration if (disconnected || timeoutError) { console.log('Connection timed out as expected without keep-alive'); expect(disconnected || timeoutError).toBeTruthy(); } else { // Some servers might not timeout quickly console.log('Server did not timeout connection (may have long timeout setting)'); await smtpClient.close(); } }); tap.test('CCM-11: Keep-alive during long operations', async () => { const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, keepAlive: true, keepAliveInterval: 2000, connectionTimeout: 10000, debug: true }); await smtpClient.connect(); // Simulate a long operation console.log('Starting simulated long operation...'); // Send initial MAIL FROM await smtpClient.sendCommand('MAIL FROM:'); // Track keep-alives during operation let keepAliveDuringOperation = 0; smtpClient.on('keepalive', () => { keepAliveDuringOperation++; }); // Simulate processing delay await new Promise(resolve => setTimeout(resolve, 5000)); // Continue with RCPT TO await smtpClient.sendCommand('RCPT TO:'); // More delay await new Promise(resolve => setTimeout(resolve, 3000)); // Should have sent keep-alives during delays expect(keepAliveDuringOperation).toBeGreaterThan(0); console.log(`Sent ${keepAliveDuringOperation} keep-alives during operation`); // Reset the session await smtpClient.sendCommand('RSET'); await smtpClient.close(); }); tap.test('CCM-11: Keep-alive interval adjustment', async () => { const intervals = [1000, 3000, 5000]; // Different intervals to test for (const interval of intervals) { console.log(`\nTesting keep-alive with ${interval}ms interval`); const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, keepAlive: true, keepAliveInterval: interval, connectionTimeout: 10000, debug: false // Less verbose for this test }); await smtpClient.connect(); let keepAliveCount = 0; let keepAliveTimes: number[] = []; let lastTime = Date.now(); smtpClient.on('keepalive', () => { const now = Date.now(); const elapsed = now - lastTime; keepAliveTimes.push(elapsed); lastTime = now; keepAliveCount++; }); // Wait for multiple intervals await new Promise(resolve => setTimeout(resolve, interval * 3.5)); // Should have sent approximately 3 keep-alives expect(keepAliveCount).toBeGreaterThanOrEqual(2); expect(keepAliveCount).toBeLessThanOrEqual(4); // Check interval accuracy (allowing 20% variance) const avgInterval = keepAliveTimes.reduce((a, b) => a + b, 0) / keepAliveTimes.length; expect(avgInterval).toBeGreaterThan(interval * 0.8); expect(avgInterval).toBeLessThan(interval * 1.2); console.log(`Sent ${keepAliveCount} keep-alives, avg interval: ${avgInterval.toFixed(0)}ms`); await smtpClient.close(); } }); tap.test('CCM-11: TCP keep-alive socket options', async () => { // Test low-level TCP keep-alive options const smtpClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, socketOptions: { keepAlive: true, keepAliveInitialDelay: 1000 }, connectionTimeout: 10000, debug: true }); let socketConfigured = false; smtpClient.on('connection', (info: any) => { if (info && info.socket && info.socket instanceof net.Socket) { // Check if keep-alive is enabled at socket level const socket = info.socket as net.Socket; // These methods might not be available in all Node versions if (typeof socket.setKeepAlive === 'function') { socket.setKeepAlive(true, 1000); socketConfigured = true; console.log('TCP keep-alive configured at socket level'); } } }); await smtpClient.connect(); // Wait a bit to ensure socket options take effect await new Promise(resolve => setTimeout(resolve, 2000)); expect(smtpClient.isConnected()).toBeTruthy(); if (!socketConfigured) { console.log('Socket-level keep-alive configuration not available'); } await smtpClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();