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; tap.test('prepare server', async () => { await startTestServer(); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('ERR-07: Exception handling - Invalid commands', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('connect', async () => { try { // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && data.includes('\r\n')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); // Test various exception-triggering commands const invalidCommands = [ 'INVALID_COMMAND_THAT_SHOULD_TRIGGER_EXCEPTION', 'MAIL FROM:<>', // Empty address 'RCPT TO:<>', // Empty address '\x00\x01\x02INVALID_BYTES', // Binary data 'VERY_LONG_COMMAND_' + 'X'.repeat(1000), // Excessively long command 'MAIL FROM', // Missing parameter 'RCPT TO', // Missing parameter 'DATA DATA DATA' // Invalid syntax ]; let exceptionHandled = false; let serverStillResponding = true; for (const command of invalidCommands) { try { socket.write(command + '\r\n'); const response = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout waiting for response')); }, 5000); socket.once('data', (chunk) => { clearTimeout(timeout); resolve(chunk.toString()); }); }); console.log(`Command: "${command.substring(0, 50)}..." -> Response: ${response.substring(0, 50)}`); // Check if server handled the exception properly if (response.includes('500') || // Command not recognized response.includes('501') || // Syntax error response.includes('502') || // Command not implemented response.includes('503') || // Bad sequence response.includes('error') || response.includes('invalid')) { exceptionHandled = true; } // Small delay between commands await new Promise(resolve => setTimeout(resolve, 100)); } catch (err) { console.log('Error with command:', command, err); // Connection might be closed by server - that's ok for some commands serverStillResponding = false; break; } } // If still connected, verify server is still responsive if (serverStillResponding) { try { socket.write('NOOP\r\n'); const noopResponse = await new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('Timeout on NOOP')); }, 5000); socket.once('data', (chunk) => { clearTimeout(timeout); resolve(chunk.toString()); }); }); if (noopResponse.includes('250')) { serverStillResponding = true; } } catch (err) { serverStillResponding = false; } } console.log('Exception handled:', exceptionHandled); console.log('Server still responding:', serverStillResponding); // Test passes if exceptions were handled OR server is still responding expect(exceptionHandled || serverStillResponding).toEqual(true); if (socket.writable) { socket.write('QUIT\r\n'); } socket.end(); done.resolve(); } catch (error) { socket.end(); done.reject(error); } }); socket.on('error', (error) => { done.reject(error); }); }); tap.test('ERR-07: Exception handling - Malformed protocol', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('connect', async () => { try { // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send commands with protocol violations const protocolViolations = [ 'EHLO', // No hostname 'MAIL FROM: SIZE=', // Incomplete SIZE 'RCPT TO: NOTIFY=', // Incomplete NOTIFY 'AUTH PLAIN', // No credentials 'STARTTLS EXTRA', // Extra parameters 'MAIL FROM:\r\nRCPT TO:', // Multiple commands in one line ]; let violationsHandled = 0; for (const violation of protocolViolations) { try { socket.write(violation + '\r\n'); const response = await new Promise((resolve) => { const timeout = setTimeout(() => { resolve('TIMEOUT'); }, 3000); socket.once('data', (chunk) => { clearTimeout(timeout); resolve(chunk.toString()); }); }); if (response !== 'TIMEOUT' && (response.includes('500') || response.includes('501') || response.includes('503'))) { violationsHandled++; } await new Promise(resolve => setTimeout(resolve, 100)); } catch (err) { // Error is ok - server might close connection } } console.log(`Protocol violations handled: ${violationsHandled}/${protocolViolations.length}`); // Server should handle at least some violations properly expect(violationsHandled).toBeGreaterThan(0); if (socket.writable) { socket.write('QUIT\r\n'); } socket.end(); done.resolve(); } catch (error) { socket.end(); done.reject(error); } }); socket.on('error', (error) => { done.reject(error); }); }); tap.test('ERR-07: Exception handling - Recovery after errors', async (tools) => { const done = tools.defer(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); socket.on('connect', async () => { try { // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Send EHLO socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ') && data.includes('\r\n')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); // Trigger an error socket.write('INVALID_COMMAND\r\n'); await new Promise((resolve) => { socket.once('data', (chunk) => { const response = chunk.toString(); expect(response).toMatch(/50[0-3]/); resolve(); }); }); // Now try a valid command sequence to ensure recovery socket.write('MAIL FROM:\r\n'); const mailResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); expect(mailResponse).toInclude('250'); socket.write('RCPT TO:\r\n'); const rcptResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); expect(rcptResponse).toInclude('250'); // Server recovered successfully after exception socket.write('RSET\r\n'); const rsetResponse = await new Promise((resolve) => { socket.once('data', (chunk) => { resolve(chunk.toString()); }); }); expect(rsetResponse).toInclude('250'); console.log('Server recovered successfully after exception'); socket.write('QUIT\r\n'); socket.end(); done.resolve(); } catch (error) { socket.end(); done.reject(error); } }); socket.on('error', (error) => { done.reject(error); }); }); tap.test('cleanup server', async () => { await stopTestServer(); }); tap.start();