import * as plugins from '@git.zone/tstest/tapbundle'; import { expect, tap } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; const TEST_PORT = 2525; let testServer; const createConnection = async (): Promise => { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 5000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); return socket; }; // Helper function to wait for SMTP response const waitForResponse = (socket: net.Socket, expectedCode?: string, timeout = 5000): Promise => { return new Promise((resolve, reject) => { let buffer = ''; const timer = setTimeout(() => { socket.removeListener('data', handler); reject(new Error(`Timeout waiting for ${expectedCode || 'any'} response`)); }, timeout); const handler = (data: Buffer) => { buffer += data.toString(); const lines = buffer.split('\r\n'); // Check if we have a complete response for (const line of lines) { if (expectedCode) { if (line.startsWith(expectedCode + ' ')) { clearTimeout(timer); socket.removeListener('data', handler); resolve(buffer); return; } } else { // Any complete response line if (line.match(/^\d{3} /)) { clearTimeout(timer); socket.removeListener('data', handler); resolve(buffer); return; } } } }; socket.on('data', handler); }); }; const getResponse = waitForResponse; const testBasicSmtpFlow = async (socket: net.Socket): Promise => { try { await waitForResponse(socket, '220'); socket.write('EHLO test.example.com\r\n'); const ehloResp = await waitForResponse(socket, '250'); if (!ehloResp.includes('250')) return false; socket.write('MAIL FROM:\r\n'); const mailResp = await waitForResponse(socket, '250'); if (!mailResp.includes('250')) return false; socket.write('RCPT TO:\r\n'); const rcptResp = await waitForResponse(socket, '250'); if (!rcptResp.includes('250')) return false; socket.write('DATA\r\n'); const dataResp = await waitForResponse(socket, '354'); if (!dataResp.includes('354')) return false; const testEmail = [ 'From: test@example.com', 'To: recipient@example.com', 'Subject: Interruption Recovery Test', '', 'This email tests server recovery after network interruption.', '.', '' ].join('\r\n'); socket.write(testEmail); const finalResp = await waitForResponse(socket, '250'); socket.write('QUIT\r\n'); socket.end(); return finalResp.includes('250'); } catch (error) { return false; } }; tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('REL-06: Network interruption - Sudden connection drop', async (tools) => { const done = tools.defer(); try { console.log('Testing sudden connection drop during session...'); // Phase 1: Create connection and drop it mid-session const socket1 = await createConnection(); await waitForResponse(socket1, '220'); socket1.write('EHLO testhost\r\n'); await waitForResponse(socket1, '250'); socket1.write('MAIL FROM:\r\n'); await waitForResponse(socket1, '250'); // Abruptly close connection during active session socket1.destroy(); console.log(' Connection abruptly closed'); // Phase 2: Test recovery await new Promise(resolve => setTimeout(resolve, 1000)); const socket2 = await createConnection(); const recoverySuccess = await testBasicSmtpFlow(socket2); expect(recoverySuccess).toEqual(true); console.log('✓ Server recovered from sudden connection drop'); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-06: Network interruption - Data transfer interruption', async (tools) => { const done = tools.defer(); try { console.log('\nTesting connection interruption during data transfer...'); const socket = await createConnection(); await waitForResponse(socket, '220'); socket.write('EHLO datatest\r\n'); await waitForResponse(socket, '250'); socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); socket.write('RCPT TO:\r\n'); await waitForResponse(socket, '250'); socket.write('DATA\r\n'); const dataResp = await waitForResponse(socket, '354'); expect(dataResp).toInclude('354'); // Start sending data but interrupt midway socket.write('From: sender@example.com\r\n'); socket.write('To: recipient@example.com\r\n'); socket.write('Subject: Interruption Test\r\n\r\n'); socket.write('This email will be interrupted...\r\n'); // Wait briefly then destroy connection (simulating network loss) await new Promise(resolve => setTimeout(resolve, 500)); socket.destroy(); console.log(' Connection interrupted during data transfer'); // Test recovery await new Promise(resolve => setTimeout(resolve, 1500)); const newSocket = await createConnection(); const recoverySuccess = await testBasicSmtpFlow(newSocket); expect(recoverySuccess).toEqual(true); console.log('✓ Server recovered from data transfer interruption'); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-06: Network interruption - Rapid reconnection attempts', async (tools) => { const done = tools.defer(); const connections: net.Socket[] = []; try { console.log('\nTesting rapid reconnection after interruptions...'); // Create and immediately destroy multiple connections console.log(' Creating 5 unstable connections...'); for (let i = 0; i < 5; i++) { try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 2000 }); connections.push(socket); // Destroy after short random delay to simulate instability setTimeout(() => socket.destroy(), 50 + Math.random() * 150); await new Promise(resolve => setTimeout(resolve, 50)); } catch (error) { // Expected - some connections might fail } } // Wait for cleanup await new Promise(resolve => setTimeout(resolve, 2000)); // Now test if server can handle normal connections let successfulConnections = 0; console.log(' Testing recovery with stable connections...'); for (let i = 0; i < 3; i++) { try { const socket = await createConnection(); const success = await testBasicSmtpFlow(socket); if (success) { successfulConnections++; } } catch (error) { console.log(` Connection ${i + 1} failed:`, error.message); } await new Promise(resolve => setTimeout(resolve, 500)); } const recoveryRate = successfulConnections / 3; console.log(` Recovery rate: ${successfulConnections}/3 (${(recoveryRate * 100).toFixed(0)}%)`); expect(recoveryRate).toBeGreaterThanOrEqual(0.66); // At least 2/3 should succeed console.log('✓ Server recovered from rapid reconnection attempts'); done.resolve(); } catch (error) { connections.forEach(conn => conn.destroy()); done.reject(error); } }); tap.test('REL-06: Network interruption - Partial command interruption', async (tools) => { const done = tools.defer(); try { console.log('\nTesting partial command transmission interruption...'); const socket = await createConnection(); await waitForResponse(socket, '220'); // Send partial EHLO command and interrupt socket.write('EH'); console.log(' Sent partial command "EH"'); await new Promise(resolve => setTimeout(resolve, 100)); socket.destroy(); console.log(' Connection destroyed with incomplete command'); // Test recovery await new Promise(resolve => setTimeout(resolve, 1000)); const newSocket = await createConnection(); const recoverySuccess = await testBasicSmtpFlow(newSocket); expect(recoverySuccess).toEqual(true); console.log('✓ Server recovered from partial command interruption'); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-06: Network interruption - Multiple interruption types', async (tools) => { const done = tools.defer(); const results: Array<{ type: string; recovered: boolean }> = []; try { console.log('\nTesting recovery from multiple interruption types...'); // Test 1: Interrupt after greeting try { const socket = await createConnection(); await waitForResponse(socket, '220'); socket.destroy(); results.push({ type: 'after-greeting', recovered: false }); } catch (e) { results.push({ type: 'after-greeting', recovered: false }); } await new Promise(resolve => setTimeout(resolve, 500)); // Test 2: Interrupt during EHLO try { const socket = await createConnection(); await waitForResponse(socket, '220'); socket.write('EHLO te'); socket.destroy(); results.push({ type: 'during-ehlo', recovered: false }); } catch (e) { results.push({ type: 'during-ehlo', recovered: false }); } await new Promise(resolve => setTimeout(resolve, 500)); // Test 3: Interrupt with invalid data try { const socket = await createConnection(); await waitForResponse(socket, '220'); socket.write('\x00\x01\x02\x03'); socket.destroy(); results.push({ type: 'invalid-data', recovered: false }); } catch (e) { results.push({ type: 'invalid-data', recovered: false }); } await new Promise(resolve => setTimeout(resolve, 1000)); // Test final recovery try { const socket = await createConnection(); const success = await testBasicSmtpFlow(socket); if (success) { // Mark all previous tests as recovered results.forEach(r => r.recovered = true); } } catch (error) { console.log('Final recovery failed:', error.message); } const recoveredCount = results.filter(r => r.recovered).length; console.log(`\nInterruption recovery summary:`); results.forEach(r => { console.log(` ${r.type}: ${r.recovered ? 'recovered' : 'failed'}`); }); expect(recoveredCount).toBeGreaterThan(0); console.log(`✓ Server recovered from ${recoveredCount}/${results.length} interruption scenarios`); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-06: Network interruption - Long delay recovery', async (tools) => { const done = tools.defer(); try { console.log('\nTesting recovery after long network interruption...'); // Create connection and start transaction const socket = await createConnection(); await waitForResponse(socket, '220'); socket.write('EHLO longdelay\r\n'); await waitForResponse(socket, '250'); socket.write('MAIL FROM:\r\n'); await waitForResponse(socket, '250'); // Simulate long network interruption socket.pause(); console.log(' Connection paused (simulating network freeze)'); await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second "freeze" // Try to continue - should fail socket.resume(); socket.write('RCPT TO:\r\n'); let continuationFailed = false; try { await waitForResponse(socket, '250', 3000); } catch (error) { continuationFailed = true; console.log(' Continuation failed as expected'); } socket.destroy(); // Test recovery with new connection const newSocket = await createConnection(); const recoverySuccess = await testBasicSmtpFlow(newSocket); expect(recoverySuccess).toEqual(true); console.log('✓ Server recovered after long network interruption'); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('cleanup server', async () => { await stopTestServer(testServer); }); export default tap.start();