update
This commit is contained in:
384
test/suite/smtpserver_commands/test.cmd-13.quit-command.ts
Normal file
384
test/suite/smtpserver_commands/test.cmd-13.quit-command.ts
Normal file
@ -0,0 +1,384 @@
|
||||
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
|
||||
tap.start();
|
Reference in New Issue
Block a user