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('CERR-09: Pool exhaustion', async () => { const pooledClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, pool: true, maxConnections: 3, maxMessages: 100, connectionTimeout: 5000, debug: true }); console.log('Testing connection pool exhaustion...'); console.log('Pool configuration: maxConnections=3'); // Track pool state const poolStats = { active: 0, idle: 0, pending: 0, created: 0, destroyed: 0 }; pooledClient.on('pool-connection-create', () => { poolStats.created++; console.log(` Pool: Connection created (total: ${poolStats.created})`); }); pooledClient.on('pool-connection-close', () => { poolStats.destroyed++; console.log(` Pool: Connection closed (total: ${poolStats.destroyed})`); }); // Send more concurrent messages than pool size const messageCount = 10; const emails = Array.from({ length: messageCount }, (_, i) => new Email({ from: 'sender@example.com', to: [`recipient${i}@example.com`], subject: `Pool test ${i}`, text: 'Testing connection pool exhaustion' })); console.log(`\nSending ${messageCount} concurrent messages...`); const startTime = Date.now(); const results = await Promise.allSettled( emails.map((email, i) => { return pooledClient.sendMail(email).then(() => { console.log(` Message ${i}: Sent`); return { index: i, status: 'sent' }; }).catch(error => { console.log(` Message ${i}: Failed - ${error.message}`); return { index: i, status: 'failed', error: error.message }; }); }) ); const elapsed = Date.now() - startTime; const successful = results.filter(r => r.status === 'fulfilled').length; const failed = results.filter(r => r.status === 'rejected').length; console.log(`\nResults after ${elapsed}ms:`); console.log(` Successful: ${successful}/${messageCount}`); console.log(` Failed: ${failed}/${messageCount}`); console.log(` Connections created: ${poolStats.created}`); console.log(` Connections destroyed: ${poolStats.destroyed}`); // Pool should limit concurrent connections expect(poolStats.created).toBeLessThanOrEqual(3); await pooledClient.close(); }); tap.test('CERR-09: Connection pool timeouts', async () => { // Create slow server const slowServer = net.createServer((socket) => { socket.write('220 Slow Server\r\n'); socket.on('data', (data) => { const command = data.toString().trim(); // Add delays to simulate slow responses setTimeout(() => { if (command.startsWith('EHLO')) { socket.write('250 OK\r\n'); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { // Slow response for other commands setTimeout(() => { socket.write('250 OK\r\n'); }, 3000); // 3 second delay } }, 1000); }); }); await new Promise((resolve) => { slowServer.listen(0, '127.0.0.1', () => resolve()); }); const slowPort = (slowServer.address() as net.AddressInfo).port; const pooledClient = createSmtpClient({ host: '127.0.0.1', port: slowPort, secure: false, pool: true, maxConnections: 2, poolTimeout: 2000, // 2 second timeout for getting connection from pool commandTimeout: 4000, debug: true }); console.log('\nTesting connection pool timeouts...'); console.log('Pool timeout: 2 seconds'); // Send multiple messages to trigger pool timeout const emails = Array.from({ length: 5 }, (_, i) => new Email({ from: 'sender@example.com', to: [`recipient${i}@example.com`], subject: `Timeout test ${i}`, text: 'Testing pool timeout' })); const timeoutErrors = []; await Promise.allSettled( emails.map(async (email, i) => { try { console.log(` Message ${i}: Attempting to send...`); await pooledClient.sendMail(email); console.log(` Message ${i}: Sent successfully`); } catch (error) { console.log(` Message ${i}: ${error.message}`); if (error.message.includes('timeout')) { timeoutErrors.push(error); } } }) ); console.log(`\nTimeout errors: ${timeoutErrors.length}`); expect(timeoutErrors.length).toBeGreaterThan(0); await pooledClient.close(); slowServer.close(); }); tap.test('CERR-09: Dead connection detection', async () => { const pooledClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, pool: true, maxConnections: 3, poolIdleTimeout: 5000, // Connections idle for 5s are closed poolPingInterval: 2000, // Ping idle connections every 2s debug: true }); console.log('\nTesting dead connection detection...'); // Track connection health checks let pingCount = 0; let deadConnections = 0; pooledClient.on('pool-connection-ping', (result) => { pingCount++; console.log(` Ping ${pingCount}: ${result.alive ? 'Connection alive' : 'Connection dead'}`); if (!result.alive) { deadConnections++; } }); // Send initial message to create connection await pooledClient.sendMail(new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'Initial message', text: 'Creating connection' })); console.log('Connection created, waiting for health checks...'); // Wait for health checks await new Promise(resolve => setTimeout(resolve, 6000)); console.log(`\nHealth check results:`); console.log(` Total pings: ${pingCount}`); console.log(` Dead connections detected: ${deadConnections}`); // Send another message to test connection recovery try { await pooledClient.sendMail(new Email({ from: 'sender@example.com', to: ['recipient@example.com'], subject: 'After idle', text: 'Testing after idle period' })); console.log('Message sent successfully after idle period'); } catch (error) { console.log('Error after idle:', error.message); } await pooledClient.close(); }); tap.test('CERR-09: Pool connection limit per host', async () => { // Create multiple servers const servers = []; for (let i = 0; i < 3; i++) { const server = net.createServer((socket) => { socket.write(`220 Server ${i + 1}\r\n`); socket.on('data', (data) => { const command = data.toString().trim(); if (command.startsWith('EHLO')) { socket.write(`250 server${i + 1}.example.com\r\n`); } else if (command === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else { socket.write('250 OK\r\n'); } }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => resolve()); }); servers.push({ server, port: (server.address() as net.AddressInfo).port }); } console.log('\nTesting per-host connection limits...'); // Create pooled client with per-host limits const pooledClient = createSmtpClient({ pool: true, maxConnections: 10, // Total pool size maxConnectionsPerHost: 2, // Per-host limit debug: true }); // Track connections per host const hostConnections: { [key: string]: number } = {}; pooledClient.on('pool-connection-create', (info) => { const host = info.host || 'unknown'; hostConnections[host] = (hostConnections[host] || 0) + 1; console.log(` Created connection to ${host} (total: ${hostConnections[host]})`); }); // Send messages to different servers const messages = []; for (let i = 0; i < 3; i++) { for (let j = 0; j < 4; j++) { messages.push({ server: i, email: new Email({ from: 'sender@example.com', to: [`recipient${j}@server${i}.com`], subject: `Test ${j} to server ${i}`, text: 'Testing per-host limits' }) }); } } // Override host/port for each message await Promise.allSettled( messages.map(async ({ server, email }) => { const client = createSmtpClient({ host: '127.0.0.1', port: servers[server].port, secure: false, pool: true, maxConnections: 10, maxConnectionsPerHost: 2, debug: false }); try { await client.sendMail(email); console.log(` Sent to server ${server + 1}`); } catch (error) { console.log(` Failed to server ${server + 1}: ${error.message}`); } await client.close(); }) ); console.log('\nConnections per host:'); Object.entries(hostConnections).forEach(([host, count]) => { console.log(` ${host}: ${count} connections`); expect(count).toBeLessThanOrEqual(2); // Should respect per-host limit }); // Clean up servers servers.forEach(s => s.server.close()); }); tap.test('CERR-09: Connection pool recovery', async () => { // Create unstable server let shouldFail = true; let requestCount = 0; const unstableServer = net.createServer((socket) => { requestCount++; if (shouldFail && requestCount <= 3) { // Abruptly close connection for first 3 requests socket.destroy(); return; } socket.write('220 Unstable Server\r\n'); socket.on('data', (data) => { 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(); } else { socket.write('250 OK\r\n'); } }); }); await new Promise((resolve) => { unstableServer.listen(0, '127.0.0.1', () => resolve()); }); const unstablePort = (unstableServer.address() as net.AddressInfo).port; const pooledClient = createSmtpClient({ host: '127.0.0.1', port: unstablePort, secure: false, pool: true, maxConnections: 2, retryFailedConnections: true, connectionRetryDelay: 1000, debug: true }); console.log('\nTesting connection pool recovery...'); console.log('Server will fail first 3 connection attempts'); // Track recovery attempts let recoveryAttempts = 0; pooledClient.on('pool-connection-retry', () => { recoveryAttempts++; console.log(` Recovery attempt ${recoveryAttempts}`); }); // Try to send messages const results = []; for (let i = 0; i < 5; i++) { const email = new Email({ from: 'sender@example.com', to: [`recipient${i}@example.com`], subject: `Recovery test ${i}`, text: 'Testing connection recovery' }); try { console.log(`\nMessage ${i}: Attempting...`); await pooledClient.sendMail(email); console.log(`Message ${i}: Success`); results.push('success'); } catch (error) { console.log(`Message ${i}: Failed - ${error.message}`); results.push('failed'); // After some failures, allow connections if (i === 2) { shouldFail = false; console.log(' Server stabilized, connections should succeed now'); } } // Small delay between attempts await new Promise(resolve => setTimeout(resolve, 500)); } console.log('\nFinal results:', results); const successCount = results.filter(r => r === 'success').length; expect(successCount).toBeGreaterThan(0); // Should recover eventually await pooledClient.close(); unstableServer.close(); }); tap.test('CERR-09: Pool metrics and monitoring', async () => { const pooledClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, pool: true, maxConnections: 5, poolMetrics: true, debug: true }); console.log('\nTesting pool metrics collection...'); // Collect metrics const metrics = { connectionsCreated: 0, connectionsDestroyed: 0, messagesQueued: 0, messagesSent: 0, errors: 0, avgWaitTime: 0, waitTimes: [] as number[] }; pooledClient.on('pool-metrics', (data) => { Object.assign(metrics, data); }); pooledClient.on('message-queued', () => { metrics.messagesQueued++; }); pooledClient.on('message-sent', (info) => { metrics.messagesSent++; if (info.waitTime) { metrics.waitTimes.push(info.waitTime); } }); // Send batch of messages const messageCount = 20; const startTime = Date.now(); await Promise.allSettled( Array.from({ length: messageCount }, (_, i) => pooledClient.sendMail(new Email({ from: 'sender@example.com', to: [`recipient${i}@example.com`], subject: `Metrics test ${i}`, text: 'Testing pool metrics' })) ) ); const totalTime = Date.now() - startTime; // Calculate average wait time if (metrics.waitTimes.length > 0) { metrics.avgWaitTime = metrics.waitTimes.reduce((a, b) => a + b, 0) / metrics.waitTimes.length; } // Get final pool status const poolStatus = pooledClient.getPoolStatus(); console.log('\nPool Metrics:'); console.log(` Messages queued: ${metrics.messagesQueued}`); console.log(` Messages sent: ${metrics.messagesSent}`); console.log(` Average wait time: ${metrics.avgWaitTime.toFixed(2)}ms`); console.log(` Total time: ${totalTime}ms`); console.log(` Throughput: ${(messageCount / totalTime * 1000).toFixed(2)} msg/sec`); console.log('\nPool Status:'); console.log(` Active connections: ${poolStatus.active}`); console.log(` Idle connections: ${poolStatus.idle}`); console.log(` Total connections: ${poolStatus.total}`); await pooledClient.close(); }); tap.test('CERR-09: Connection affinity', async () => { const pooledClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, pool: true, maxConnections: 3, connectionAffinity: 'sender', // Reuse same connection for same sender debug: true }); console.log('\nTesting connection affinity...'); // Track which connection handles which sender const senderConnections: { [sender: string]: string } = {}; pooledClient.on('connection-assigned', (info) => { senderConnections[info.sender] = info.connectionId; console.log(` Sender ${info.sender} assigned to connection ${info.connectionId}`); }); // Send messages from different senders const senders = ['alice@example.com', 'bob@example.com', 'alice@example.com', 'charlie@example.com', 'bob@example.com']; for (const sender of senders) { const email = new Email({ from: sender, to: ['recipient@example.com'], subject: `From ${sender}`, text: 'Testing connection affinity' }); await pooledClient.sendMail(email); const connectionId = senderConnections[sender]; console.log(` Message from ${sender} sent via connection ${connectionId}`); } // Verify affinity console.log('\nConnection affinity results:'); const uniqueSenders = [...new Set(senders)]; uniqueSenders.forEach(sender => { const messages = senders.filter(s => s === sender).length; console.log(` ${sender}: ${messages} messages, connection ${senderConnections[sender]}`); }); await pooledClient.close(); }); tap.test('CERR-09: Pool resource cleanup', async () => { const pooledClient = createSmtpClient({ host: testServer.hostname, port: testServer.port, secure: false, pool: true, maxConnections: 3, poolCleanupInterval: 1000, // Clean up every second debug: true }); console.log('\nTesting pool resource cleanup...'); // Track cleanup events const cleanupStats = { idleClosed: 0, staleClosed: 0, errorClosed: 0 }; pooledClient.on('pool-connection-cleanup', (reason) => { switch (reason.type) { case 'idle': cleanupStats.idleClosed++; console.log(` Closed idle connection: ${reason.connectionId}`); break; case 'stale': cleanupStats.staleClosed++; console.log(` Closed stale connection: ${reason.connectionId}`); break; case 'error': cleanupStats.errorClosed++; console.log(` Closed errored connection: ${reason.connectionId}`); break; } }); // Send some messages for (let i = 0; i < 3; i++) { await pooledClient.sendMail(new Email({ from: 'sender@example.com', to: [`recipient${i}@example.com`], subject: `Cleanup test ${i}`, text: 'Testing cleanup' })); } console.log('Messages sent, waiting for cleanup...'); // Wait for cleanup cycles await new Promise(resolve => setTimeout(resolve, 5000)); console.log('\nCleanup statistics:'); console.log(` Idle connections closed: ${cleanupStats.idleClosed}`); console.log(` Stale connections closed: ${cleanupStats.staleClosed}`); console.log(` Errored connections closed: ${cleanupStats.errorClosed}`); const finalStatus = pooledClient.getPoolStatus(); console.log(`\nFinal pool status:`); console.log(` Active: ${finalStatus.active}`); console.log(` Idle: ${finalStatus.idle}`); console.log(` Total: ${finalStatus.total}`); await pooledClient.close(); }); tap.test('cleanup test SMTP server', async () => { if (testServer) { await testServer.stop(); } }); export default tap.start();