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; tap.test('prepare server', async () => { testServer = await startTestServer({ port: TEST_PORT }); await new Promise(resolve => setTimeout(resolve, 100)); }); tap.test('PERF-05: Connection processing time - Connection establishment', async (tools) => { const done = tools.defer(); const testConnections = 10; const connectionTimes: number[] = []; try { console.log(`Testing connection establishment time for ${testConnections} connections...`); for (let i = 0; i < testConnections; i++) { const connectionStart = Date.now(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', () => { const connectionTime = Date.now() - connectionStart; connectionTimes.push(connectionTime); resolve(); }); socket.once('error', reject); }); // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); // Clean close socket.write('QUIT\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); socket.end(); // Small delay between connections await new Promise(resolve => setTimeout(resolve, 50)); } // Calculate statistics const avgConnectionTime = connectionTimes.reduce((a, b) => a + b, 0) / connectionTimes.length; const minConnectionTime = Math.min(...connectionTimes); const maxConnectionTime = Math.max(...connectionTimes); console.log(`\nConnection Establishment Results:`); console.log(`Average: ${avgConnectionTime.toFixed(0)}ms`); console.log(`Min: ${minConnectionTime}ms`); console.log(`Max: ${maxConnectionTime}ms`); console.log(`All times: ${connectionTimes.join(', ')}ms`); // Test passes if average connection time is less than 1000ms expect(avgConnectionTime).toBeLessThan(1000); done.resolve(); } catch (error) { done.reject(error); } }); tap.test('PERF-05: Connection processing time - Transaction processing', async (tools) => { const done = tools.defer(); const testTransactions = 10; const processingTimes: number[] = []; const fullTransactionTimes: number[] = []; // Add a timeout to prevent test from hanging const testTimeout = setTimeout(() => { console.log('Test timeout reached, moving on...'); done.resolve(); }, 30000); // 30 second timeout try { console.log(`\nTesting transaction processing time for ${testTransactions} transactions...`); for (let i = 0; i < testTransactions; i++) { const fullTransactionStart = Date.now(); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await new Promise((resolve) => { socket.once('data', () => resolve()); }); const processingStart = Date.now(); // Send EHLO socket.write(`EHLO testhost-perf-${i}\r\n`); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); // Look for the end of EHLO response (250 without dash) if (data.includes('250 ')) { socket.removeListener('data', handleData); resolve(); } }; socket.on('data', handleData); }); // Send MAIL FROM socket.write(`MAIL FROM:\r\n`); await new Promise((resolve, reject) => { let mailResponse = ''; const handleMailResponse = (chunk: Buffer) => { mailResponse += chunk.toString(); if (mailResponse.includes('\r\n')) { socket.removeListener('data', handleMailResponse); if (mailResponse.includes('250')) { resolve(); } else { reject(new Error(`MAIL FROM failed: ${mailResponse}`)); } } }; socket.on('data', handleMailResponse); }); // Send RCPT TO socket.write(`RCPT TO:\r\n`); await new Promise((resolve, reject) => { let rcptResponse = ''; const handleRcptResponse = (chunk: Buffer) => { rcptResponse += chunk.toString(); if (rcptResponse.includes('\r\n')) { socket.removeListener('data', handleRcptResponse); if (rcptResponse.includes('250')) { resolve(); } else { reject(new Error(`RCPT TO failed: ${rcptResponse}`)); } } }; socket.on('data', handleRcptResponse); }); // Send DATA socket.write('DATA\r\n'); await new Promise((resolve, reject) => { let dataResponse = ''; const handleDataResponse = (chunk: Buffer) => { dataResponse += chunk.toString(); if (dataResponse.includes('\r\n')) { socket.removeListener('data', handleDataResponse); if (dataResponse.includes('354')) { resolve(); } else { reject(new Error(`DATA failed: ${dataResponse}`)); } } }; socket.on('data', handleDataResponse); }); // Send email content const emailContent = [ `From: sender${i}@example.com`, `To: recipient${i}@example.com`, `Subject: Connection Processing Test ${i}`, '', 'Connection processing time test.', '.', '' ].join('\r\n'); socket.write(emailContent); await new Promise((resolve, reject) => { let submitResponse = ''; const handleSubmitResponse = (chunk: Buffer) => { submitResponse += chunk.toString(); if (submitResponse.includes('\r\n') && submitResponse.includes('250')) { socket.removeListener('data', handleSubmitResponse); resolve(); } else if (submitResponse.includes('\r\n') && (submitResponse.includes('4') || submitResponse.includes('5'))) { socket.removeListener('data', handleSubmitResponse); reject(new Error(`Message submission failed: ${submitResponse}`)); } }; socket.on('data', handleSubmitResponse); }); const processingTime = Date.now() - processingStart; processingTimes.push(processingTime); // Send QUIT socket.write('QUIT\r\n'); await new Promise((resolve) => { socket.once('data', () => resolve()); }); socket.end(); const fullTransactionTime = Date.now() - fullTransactionStart; fullTransactionTimes.push(fullTransactionTime); // Small delay between transactions await new Promise(resolve => setTimeout(resolve, 50)); } // Calculate statistics const avgProcessingTime = processingTimes.reduce((a, b) => a + b, 0) / processingTimes.length; const minProcessingTime = Math.min(...processingTimes); const maxProcessingTime = Math.max(...processingTimes); const avgFullTime = fullTransactionTimes.reduce((a, b) => a + b, 0) / fullTransactionTimes.length; console.log(`\nTransaction Processing Results:`); console.log(`Average processing: ${avgProcessingTime.toFixed(0)}ms`); console.log(`Min processing: ${minProcessingTime}ms`); console.log(`Max processing: ${maxProcessingTime}ms`); console.log(`Average full transaction: ${avgFullTime.toFixed(0)}ms`); // Test passes if average processing time is less than 2000ms expect(avgProcessingTime).toBeLessThan(2000); clearTimeout(testTimeout); done.resolve(); } catch (error) { clearTimeout(testTimeout); done.reject(error); } }); tap.test('PERF-05: Connection processing time - Command response times', async (tools) => { const done = tools.defer(); const commandTimings: { [key: string]: number[] } = { EHLO: [], NOOP: [] }; // Add a timeout to prevent test from hanging const testTimeout = setTimeout(() => { console.log('Command timing test timeout reached, moving on...'); done.resolve(); }, 20000); // 20 second timeout try { console.log(`\nMeasuring individual command response times...`); const socket = net.createConnection({ host: 'localhost', port: TEST_PORT, timeout: 30000 }); await new Promise((resolve, reject) => { socket.once('connect', resolve); socket.once('error', reject); }); // Read greeting await new Promise((resolve) => { let greeting = ''; const handleGreeting = (chunk: Buffer) => { greeting += chunk.toString(); if (greeting.includes('220') && greeting.includes('\r\n')) { socket.removeListener('data', handleGreeting); resolve(); } }; socket.on('data', handleGreeting); }); // Measure EHLO response times for (let i = 0; i < 3; i++) { const start = Date.now(); socket.write('EHLO testhost\r\n'); await new Promise((resolve) => { let data = ''; const handleData = (chunk: Buffer) => { data += chunk.toString(); if (data.includes('250 ')) { socket.removeListener('data', handleData); commandTimings.EHLO.push(Date.now() - start); resolve(); } }; socket.on('data', handleData); }); } // Measure NOOP response times for (let i = 0; i < 3; i++) { const start = Date.now(); socket.write('NOOP\r\n'); await new Promise((resolve) => { let noopResponse = ''; const handleNoop = (chunk: Buffer) => { noopResponse += chunk.toString(); if (noopResponse.includes('\r\n')) { socket.removeListener('data', handleNoop); commandTimings.NOOP.push(Date.now() - start); resolve(); } }; socket.on('data', handleNoop); }); } // Close connection socket.write('QUIT\r\n'); await new Promise((resolve) => { socket.once('data', () => { socket.end(); resolve(); }); }); // Calculate and display results console.log(`\nCommand Response Times (ms):`); for (const [command, times] of Object.entries(commandTimings)) { if (times.length > 0) { const avg = times.reduce((a, b) => a + b, 0) / times.length; console.log(`${command}: avg=${avg.toFixed(0)}, samples=[${times.join(', ')}]`); // All commands should respond in less than 500ms on average expect(avg).toBeLessThan(500); } } clearTimeout(testTimeout); done.resolve(); } catch (error) { clearTimeout(testTimeout); done.reject(error); } }); tap.test('cleanup server', async () => { await stopTestServer(testServer); }); tap.start();