dcrouter/test/suite/smtpserver_commands/test.cmd-13.quit-command.ts
2025-05-25 19:05:43 +00:00

384 lines
10 KiB
TypeScript

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:<sender@example.com>\r\n');
} else if (currentStep === 'mail_from' && receivedData.includes('250')) {
currentStep = 'rcpt_to';
socket.write('RCPT TO:<recipient@example.com>\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();