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; }; const getResponse = (socket: net.Socket, commandName: string): Promise => { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error(`${commandName} response timeout`)); }, 3000); socket.once('data', (chunk: Buffer) => { clearTimeout(timeout); resolve(chunk.toString()); }); }); }; const testBasicSmtpFlow = async (socket: net.Socket): Promise => { try { await getResponse(socket, 'GREETING'); socket.write('EHLO test.example.com\r\n'); const ehloResp = await getResponse(socket, 'EHLO'); if (!ehloResp.includes('250')) return false; // Wait for complete EHLO response if (ehloResp.includes('250-')) { await new Promise(resolve => setTimeout(resolve, 100)); } socket.write('MAIL FROM:\r\n'); const mailResp = await getResponse(socket, 'MAIL FROM'); if (!mailResp.includes('250')) return false; socket.write('RCPT TO:\r\n'); const rcptResp = await getResponse(socket, 'RCPT TO'); if (!rcptResp.includes('250')) return false; socket.write('DATA\r\n'); const dataResp = await getResponse(socket, 'DATA'); 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 getResponse(socket, 'EMAIL DATA'); 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 getResponse(socket1, 'GREETING'); socket1.write('EHLO testhost\r\n'); let data = ''; await new Promise((resolve) => { const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket1.removeListener('data', handleData); resolve(); } }; socket1.on('data', handleData); }); socket1.write('MAIL FROM:\r\n'); await getResponse(socket1, 'MAIL FROM'); // 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 getResponse(socket, 'GREETING'); socket.write('EHLO datatest\r\n'); let data = ''; await new Promise((resolve) => { const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); socket.write('MAIL FROM:\r\n'); await getResponse(socket, 'MAIL FROM'); socket.write('RCPT TO:\r\n'); await getResponse(socket, 'RCPT TO'); socket.write('DATA\r\n'); const dataResp = await getResponse(socket, 'DATA'); 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 getResponse(socket, 'GREETING'); // 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 getResponse(socket, 'GREETING'); 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 getResponse(socket, 'GREETING'); 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 getResponse(socket, 'GREETING'); 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 getResponse(socket, 'GREETING'); socket.write('EHLO longdelay\r\n'); let data = ''; await new Promise((resolve) => { const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && !data.includes('250-')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); socket.write('MAIL FROM:\r\n'); await getResponse(socket, 'MAIL FROM'); // 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 getResponse(socket, 'RCPT TO'); } 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); }); tap.start();