import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { createTestSmtpClient } from '../../helpers/smtp.client.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; tap.test('CREL-04: Basic Connection Recovery from Server Issues', async () => { console.log('\nπŸ’₯ Testing SMTP Client Connection Recovery'); console.log('=' .repeat(60)); console.log('\nπŸ”Œ Testing recovery from connection drops...'); let connectionCount = 0; let dropConnections = false; // Create test server that can simulate connection drops const server = net.createServer(socket => { connectionCount++; console.log(` [Server] Connection ${connectionCount} established`); if (dropConnections && connectionCount > 2) { console.log(` [Server] Simulating connection drop for connection ${connectionCount}`); setTimeout(() => { socket.destroy(); }, 100); return; } socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { console.log(' Creating SMTP client with connection recovery settings...'); const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 2, maxMessages: 50, connectionTimeout: 2000 }); const emails = []; for (let i = 0; i < 8; i++) { emails.push(new Email({ from: 'sender@crashtest.example', to: [`recipient${i}@crashtest.example`], subject: `Connection Recovery Test ${i + 1}`, text: `Testing connection recovery, email ${i + 1}` })); } console.log(' Phase 1: Sending initial emails (connections should succeed)...'); const results1 = []; for (let i = 0; i < 3; i++) { try { await smtpClient.sendMail(emails[i]); results1.push({ success: true, index: i }); console.log(` βœ“ Email ${i + 1} sent successfully`); } catch (error) { results1.push({ success: false, index: i, error }); console.log(` βœ— Email ${i + 1} failed: ${error.message}`); } } console.log(' Phase 2: Enabling connection drops...'); dropConnections = true; console.log(' Sending emails during connection instability...'); const results2 = []; const promises = emails.slice(3).map((email, index) => { const actualIndex = index + 3; return smtpClient.sendMail(email).then(result => { console.log(` βœ“ Email ${actualIndex + 1} recovered and sent`); return { success: true, index: actualIndex, result }; }).catch(error => { console.log(` βœ— Email ${actualIndex + 1} failed permanently: ${error.message}`); return { success: false, index: actualIndex, error }; }); }); const results2Resolved = await Promise.all(promises); results2.push(...results2Resolved); const totalSuccessful = [...results1, ...results2].filter(r => r.success).length; const totalFailed = [...results1, ...results2].filter(r => !r.success).length; console.log(` Connection attempts: ${connectionCount}`); console.log(` Emails sent successfully: ${totalSuccessful}/${emails.length}`); console.log(` Failed emails: ${totalFailed}`); console.log(` Recovery effectiveness: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`); expect(totalSuccessful).toBeGreaterThanOrEqual(3); // At least initial emails should succeed expect(connectionCount).toBeGreaterThanOrEqual(2); // Should have made multiple connection attempts smtpClient.close(); } finally { server.close(); } }); tap.test('CREL-04: Recovery from Server Restart', async () => { console.log('\nπŸ’€ Testing recovery from server restart...'); // Start first server instance let server1 = net.createServer(socket => { console.log(' [Server1] Connection established'); socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server1.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server1.address() as net.AddressInfo).port; try { console.log(' Creating client...'); const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 1, connectionTimeout: 3000 }); const emails = []; for (let i = 0; i < 6; i++) { emails.push(new Email({ from: 'sender@serverrestart.test', to: [`recipient${i}@serverrestart.test`], subject: `Server Restart Recovery ${i + 1}`, text: `Testing server restart recovery, email ${i + 1}` })); } console.log(' Sending first batch of emails...'); await smtpClient.sendMail(emails[0]); console.log(' βœ“ Email 1 sent successfully'); await smtpClient.sendMail(emails[1]); console.log(' βœ“ Email 2 sent successfully'); console.log(' Simulating server restart by closing server...'); server1.close(); await new Promise(resolve => setTimeout(resolve, 500)); console.log(' Starting new server instance on same port...'); const server2 = net.createServer(socket => { console.log(' [Server2] Connection established after restart'); socket.write('220 localhost SMTP Test Server Restarted\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server2.listen(port, '127.0.0.1', () => { resolve(); }); }); console.log(' Sending emails after server restart...'); const recoveryResults = []; for (let i = 2; i < emails.length; i++) { try { await smtpClient.sendMail(emails[i]); recoveryResults.push({ success: true, index: i }); console.log(` βœ“ Email ${i + 1} sent after server recovery`); } catch (error) { recoveryResults.push({ success: false, index: i, error }); console.log(` βœ— Email ${i + 1} failed: ${error.message}`); } } const successfulRecovery = recoveryResults.filter(r => r.success).length; const totalSuccessful = 2 + successfulRecovery; // 2 from before restart + recovery console.log(` Pre-restart emails: 2/2 successful`); console.log(` Post-restart emails: ${successfulRecovery}/${recoveryResults.length} successful`); console.log(` Overall success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`); console.log(` Server restart recovery: ${successfulRecovery > 0 ? 'Successful' : 'Failed'}`); expect(successfulRecovery).toBeGreaterThanOrEqual(1); // At least some emails should work after restart smtpClient.close(); server2.close(); } finally { // Ensure cleanup try { server1.close(); } catch (e) { /* Already closed */ } } }); tap.test('CREL-04: Error Recovery and State Management', async () => { console.log('\n⚠️ Testing error recovery and state management...'); let errorInjectionEnabled = false; const server = net.createServer(socket => { socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (errorInjectionEnabled && line.startsWith('MAIL FROM')) { console.log(' [Server] Injecting error response'); socket.write('550 Simulated server error\r\n'); return; } if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } else if (line === 'RSET') { socket.write('250 OK\r\n'); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { console.log(' Creating client with error handling...'); const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 1, connectionTimeout: 3000 }); const emails = []; for (let i = 0; i < 6; i++) { emails.push(new Email({ from: 'sender@exception.test', to: [`recipient${i}@exception.test`], subject: `Error Recovery Test ${i + 1}`, text: `Testing error recovery, email ${i + 1}` })); } console.log(' Phase 1: Sending emails normally...'); await smtpClient.sendMail(emails[0]); console.log(' βœ“ Email 1 sent successfully'); await smtpClient.sendMail(emails[1]); console.log(' βœ“ Email 2 sent successfully'); console.log(' Phase 2: Enabling error injection...'); errorInjectionEnabled = true; console.log(' Sending emails with error injection...'); const recoveryResults = []; for (let i = 2; i < 4; i++) { try { await smtpClient.sendMail(emails[i]); recoveryResults.push({ success: true, index: i }); console.log(` βœ“ Email ${i + 1} sent despite errors`); } catch (error) { recoveryResults.push({ success: false, index: i, error }); console.log(` βœ— Email ${i + 1} failed: ${error.message}`); } } console.log(' Phase 3: Disabling error injection...'); errorInjectionEnabled = false; console.log(' Sending final emails (recovery validation)...'); for (let i = 4; i < emails.length; i++) { try { await smtpClient.sendMail(emails[i]); recoveryResults.push({ success: true, index: i }); console.log(` βœ“ Email ${i + 1} sent after recovery`); } catch (error) { recoveryResults.push({ success: false, index: i, error }); console.log(` βœ— Email ${i + 1} failed: ${error.message}`); } } const successful = recoveryResults.filter(r => r.success).length; const totalSuccessful = 2 + successful; // 2 initial + recovery phase console.log(` Pre-error emails: 2/2 successful`); console.log(` Error/recovery phase emails: ${successful}/${recoveryResults.length} successful`); console.log(` Total success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`); console.log(` Error recovery: ${successful >= recoveryResults.length - 2 ? 'Effective' : 'Partial'}`); expect(totalSuccessful).toBeGreaterThanOrEqual(4); // At least initial + some recovery smtpClient.close(); } finally { server.close(); } }); tap.test('CREL-04: Resource Management During Issues', async () => { console.log('\n🧠 Testing resource management during connection issues...'); let memoryBefore = process.memoryUsage(); const server = net.createServer(socket => { socket.write('220 localhost SMTP Test Server\r\n'); socket.on('data', (data) => { const lines = data.toString().split('\r\n'); lines.forEach(line => { if (line.startsWith('EHLO') || line.startsWith('HELO')) { socket.write('250-localhost\r\n'); socket.write('250 SIZE 10485760\r\n'); } else if (line.startsWith('MAIL FROM:')) { socket.write('250 OK\r\n'); } else if (line.startsWith('RCPT TO:')) { socket.write('250 OK\r\n'); } else if (line === 'DATA') { socket.write('354 Send data\r\n'); } else if (line === '.') { socket.write('250 OK Message accepted\r\n'); } else if (line === 'QUIT') { socket.write('221 Bye\r\n'); socket.end(); } }); }); }); await new Promise((resolve) => { server.listen(0, '127.0.0.1', () => { resolve(); }); }); const port = (server.address() as net.AddressInfo).port; try { console.log(' Creating client for resource management test...'); const smtpClient = createTestSmtpClient({ host: '127.0.0.1', port: port, secure: false, maxConnections: 5, maxMessages: 100 }); console.log(' Creating emails with various content types...'); const emails = [ new Email({ from: 'sender@resource.test', to: ['recipient1@resource.test'], subject: 'Resource Test - Normal', text: 'Normal email content' }), new Email({ from: 'sender@resource.test', to: ['recipient2@resource.test'], subject: 'Resource Test - Large Content', text: 'X'.repeat(50000) // Large content }), new Email({ from: 'sender@resource.test', to: ['recipient3@resource.test'], subject: 'Resource Test - Unicode', text: '🎭πŸŽͺ🎨🎯🎲🎸🎺🎻🎼🎡🎢🎷'.repeat(100) }) ]; console.log(' Sending emails and monitoring resource usage...'); const results = []; for (let i = 0; i < emails.length; i++) { console.log(` Testing email ${i + 1} (${emails[i].subject.split(' - ')[1]})...`); try { // Monitor memory usage before sending const memBefore = process.memoryUsage(); console.log(` Memory before: ${Math.round(memBefore.heapUsed / 1024 / 1024)}MB`); await smtpClient.sendMail(emails[i]); const memAfter = process.memoryUsage(); console.log(` Memory after: ${Math.round(memAfter.heapUsed / 1024 / 1024)}MB`); const memIncrease = memAfter.heapUsed - memBefore.heapUsed; console.log(` Memory increase: ${Math.round(memIncrease / 1024)}KB`); results.push({ success: true, index: i, memoryIncrease: memIncrease }); console.log(` βœ“ Email ${i + 1} sent successfully`); } catch (error) { results.push({ success: false, index: i, error }); console.log(` βœ— Email ${i + 1} failed: ${error.message}`); } // Force garbage collection if available if (global.gc) { global.gc(); } await new Promise(resolve => setTimeout(resolve, 100)); } const successful = results.filter(r => r.success).length; const totalMemoryIncrease = results.reduce((sum, r) => sum + (r.memoryIncrease || 0), 0); console.log(` Resource management: ${successful}/${emails.length} emails processed`); console.log(` Total memory increase: ${Math.round(totalMemoryIncrease / 1024)}KB`); console.log(` Resource efficiency: ${((successful / emails.length) * 100).toFixed(1)}%`); expect(successful).toBeGreaterThanOrEqual(2); // Most emails should succeed expect(totalMemoryIncrease).toBeLessThan(100 * 1024 * 1024); // Less than 100MB increase smtpClient.close(); } finally { server.close(); } }); tap.test('CREL-04: Test Summary', async () => { console.log('\nβœ… CREL-04: Crash Recovery Reliability Tests completed'); console.log('πŸ’₯ All connection recovery scenarios tested successfully'); }); tap.start();