import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as net from 'net'; import * as path from 'path'; import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'; // Test configuration const TEST_PORT = 2525; let testServer; const TEST_TIMEOUT = 10000; // Setup tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); // Test: Basic QUIT command tap.test('QUIT - should close connection gracefully', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let connectionClosed = false; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'quit'; socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { // Don't destroy immediately, wait for server to close connection setTimeout(() => { if (!connectionClosed) { socket.destroy(); expect(receivedData).toInclude('221'); // Closing connection message done.resolve(); } }, 2000); } }); socket.on('close', () => { if (currentStep === 'quit' && receivedData.includes('221')) { connectionClosed = true; expect(receivedData).toInclude('221'); done.resolve(); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: QUIT during transaction tap.test('QUIT - should work during active transaction', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'mail_from'; socket.write('MAIL FROM:\r\n'); } else if (currentStep === 'mail_from' && receivedData.includes('250')) { currentStep = 'rcpt_to'; socket.write('RCPT TO:\r\n'); } else if (currentStep === 'rcpt_to' && receivedData.includes('250')) { currentStep = 'quit'; socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('221'); done.resolve(); }, 100); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: QUIT immediately after connect tap.test('QUIT - should work immediately after connection', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'quit'; socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { setTimeout(() => { socket.destroy(); expect(receivedData).toInclude('221'); done.resolve(); }, 100); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: QUIT with parameters (should be ignored or rejected) tap.test('QUIT - should handle QUIT with parameters', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'quit_with_param'; receivedData = ''; socket.write('QUIT unexpected parameter\r\n'); } else if (currentStep === 'quit_with_param' && (receivedData.includes('221') || receivedData.includes('501'))) { // Server may accept (221) or reject (501) QUIT with parameters const responseCode = receivedData.match(/(\d{3})/)?.[1]; socket.destroy(); expect(['221', '501']).toInclude(responseCode); done.resolve(); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: Multiple QUITs (second should fail) tap.test('QUIT - second QUIT should fail after connection closed', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let quitSent = false; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; receivedData = ''; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250 ')) { currentStep = 'quit'; receivedData = ''; socket.write('QUIT\r\n'); quitSent = true; } else if (currentStep === 'quit' && receivedData.includes('221')) { // Try to send another QUIT try { socket.write('QUIT\r\n'); // If write succeeds, wait a bit to see if we get a response setTimeout(() => { socket.destroy(); done.resolve(); // Test passes either way }, 500); } catch (err) { // Write failed because connection closed - this is expected done.resolve(); } } }); socket.on('close', () => { if (quitSent) { done.resolve(); } }); socket.on('error', (error) => { if (quitSent && error.message.includes('EPIPE')) { // Expected error when writing to closed socket done.resolve(); } else { done.reject(error); } }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: QUIT response format tap.test('QUIT - should return proper 221 response', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let quitResponse = ''; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'quit'; receivedData = ''; // Clear buffer to capture only QUIT response socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { quitResponse = receivedData.trim(); setTimeout(() => { socket.destroy(); expect(quitResponse).toStartWith('221'); expect(quitResponse.toLowerCase()).toInclude('closing'); done.resolve(); }, 100); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Test: Connection cleanup after QUIT tap.test('QUIT - verify clean connection shutdown', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: TEST_TIMEOUT }); let receivedData = ''; let currentStep = 'connecting'; let closeEventFired = false; let endEventFired = false; socket.on('data', (data) => { receivedData += data.toString(); if (currentStep === 'connecting' && receivedData.includes('220')) { currentStep = 'ehlo'; socket.write('EHLO test.example.com\r\n'); } else if (currentStep === 'ehlo' && receivedData.includes('250')) { currentStep = 'quit'; socket.write('QUIT\r\n'); } else if (currentStep === 'quit' && receivedData.includes('221')) { // Wait for clean shutdown setTimeout(() => { if (!closeEventFired && !endEventFired) { socket.destroy(); done.resolve(); } }, 3000); } }); socket.on('end', () => { endEventFired = true; }); socket.on('close', () => { closeEventFired = true; if (currentStep === 'quit') { expect(endEventFired || closeEventFired).toEqual(true); done.resolve(); } }); socket.on('error', (error) => { done.reject(error); }); socket.on('timeout', () => { socket.destroy(); done.reject(new Error(`Connection timeout at step: ${currentStep}`)); }); await done.promise; }); // Teardown tap.test('cleanup server', async () => { await stopTestServer(testServer); }); // Start the test export default tap.start();