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; // 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); }); }; tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('REL-02: Restart recovery - Server state after restart', async (tools) => { const done = tools.defer(); try { console.log('Testing server state and recovery capabilities...'); // First, establish that server is working normally const socket1 = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket1.once('connect', resolve); socket1.once('error', reject); }); // Read greeting const greeting1 = await waitForResponse(socket1, '220'); expect(greeting1).toInclude('220'); console.log('Initial connection successful'); // Send EHLO socket1.write('EHLO testhost\r\n'); await waitForResponse(socket1, '250'); // Complete a transaction socket1.write('MAIL FROM:\r\n'); const mailResp1 = await waitForResponse(socket1, '250'); expect(mailResp1).toInclude('250'); socket1.write('RCPT TO:\r\n'); const rcptResp1 = await waitForResponse(socket1, '250'); expect(rcptResp1).toInclude('250'); socket1.write('DATA\r\n'); const dataResp1 = await waitForResponse(socket1, '354'); expect(dataResp1).toInclude('354'); const emailContent = [ 'From: sender@example.com', 'To: recipient@example.com', 'Subject: Pre-restart test', '', 'Testing server state before restart.', '.', '' ].join('\r\n'); socket1.write(emailContent); const sendResp1 = await waitForResponse(socket1, '250'); expect(sendResp1).toInclude('250'); socket1.write('QUIT\r\n'); await waitForResponse(socket1, '221'); socket1.end(); console.log('Pre-restart transaction completed successfully'); // Simulate server restart by closing and reopening connections console.log('\nSimulating server restart scenario...'); // Wait a moment to simulate restart time await new Promise(resolve => setTimeout(resolve, 2000)); // Test recovery after simulated restart const socket2 = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket2.once('connect', resolve); socket2.once('error', reject); }); // Read greeting after "restart" const greeting2 = await new Promise((resolve) => { socket2.once('data', (chunk) => { resolve(chunk.toString()); }); }); expect(greeting2).toInclude('220'); console.log('Post-restart connection successful'); // Verify server is fully functional after restart socket2.write('EHLO testhost-postrestart\r\n'); await waitForResponse(socket2, '250'); // Complete another transaction to verify full recovery socket2.write('MAIL FROM:\r\n'); const mailResp2 = await waitForResponse(socket2, '250'); expect(mailResp2).toInclude('250'); socket2.write('RCPT TO:\r\n'); const rcptResp2 = await waitForResponse(socket2, '250'); expect(rcptResp2).toInclude('250'); socket2.write('DATA\r\n'); const dataResp2 = await waitForResponse(socket2, '354'); expect(dataResp2).toInclude('354'); const postRestartEmail = [ 'From: sender2@example.com', 'To: recipient2@example.com', 'Subject: Post-restart recovery test', '', 'Testing server recovery after restart.', '.', '' ].join('\r\n'); socket2.write(postRestartEmail); const sendResp2 = await waitForResponse(socket2, '250'); expect(sendResp2).toInclude('250'); socket2.write('QUIT\r\n'); await waitForResponse(socket2, '221'); socket2.end(); console.log('Post-restart transaction completed successfully'); console.log('Server recovered successfully from restart'); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-02: Restart recovery - Multiple rapid reconnections', async (tools) => { const done = tools.defer(); const rapidConnections = 10; let successfulReconnects = 0; try { console.log(`\nTesting rapid reconnection after disruption (${rapidConnections} attempts)...`); for (let i = 0; i < rapidConnections; i++) { try { const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 5000 }); await new Promise((resolve, reject) => { const timeout = setTimeout(() => { socket.destroy(); reject(new Error('Connection timeout')); }, 5000); socket.once('connect', () => { clearTimeout(timeout); resolve(); }); socket.once('error', (err) => { clearTimeout(timeout); reject(err); }); }); // Read greeting try { const greeting = await waitForResponse(socket, '220', 3000); if (greeting.includes('220')) { successfulReconnects++; socket.write('QUIT\r\n'); await waitForResponse(socket, '221', 1000).catch(() => {}); socket.end(); } else { socket.destroy(); } } catch (error) { socket.destroy(); throw error; } // Very short delay between attempts await new Promise(resolve => setTimeout(resolve, 100)); } catch (error) { console.log(`Reconnection ${i + 1} failed:`, error.message); } } const reconnectRate = successfulReconnects / rapidConnections; console.log(`Successful reconnections: ${successfulReconnects}/${rapidConnections} (${(reconnectRate * 100).toFixed(1)}%)`); // Expect high success rate for good recovery expect(reconnectRate).toBeGreaterThanOrEqual(0.8); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('REL-02: Restart recovery - State persistence check', async (tools) => { const done = tools.defer(); try { console.log('\nTesting server state persistence across connections...'); // Create initial connection and start transaction const socket1 = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket1.once('connect', resolve); socket1.once('error', reject); }); // Read greeting await waitForResponse(socket1, '220'); // Send EHLO socket1.write('EHLO persistence-test\r\n'); await waitForResponse(socket1, '250'); // Start transaction but don't complete it socket1.write('MAIL FROM:\r\n'); const mailResp = await waitForResponse(socket1, '250'); expect(mailResp).toInclude('250'); // Abruptly close connection socket1.destroy(); console.log('Abruptly closed connection with incomplete transaction'); // Wait briefly await new Promise(resolve => setTimeout(resolve, 1000)); // Create new connection and verify server recovered const socket2 = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket2.once('connect', resolve); socket2.once('error', reject); }); // Read greeting await waitForResponse(socket2, '220'); // Send EHLO socket2.write('EHLO recovery-test\r\n'); await waitForResponse(socket2, '250'); // Try new transaction - should work without issues from previous incomplete one socket2.write('MAIL FROM:\r\n'); const mailResponse = await waitForResponse(socket2, '250'); expect(mailResponse).toInclude('250'); console.log('Server recovered successfully - new transaction started without issues'); socket2.write('QUIT\r\n'); await waitForResponse(socket2, '221'); socket2.end(); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('cleanup server', async () => { await stopTestServer(testServer); }); tap.start();