update
This commit is contained in:
@ -0,0 +1,321 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { startTestServer, stopTestServer } from '../../helpers/server.loader.js'
|
||||
import type { ITestServer } from '../../helpers/server.loader.js';
|
||||
const TEST_PORT = 2525;
|
||||
const TEST_TIMEOUT = 30000;
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for abrupt disconnection tests', async () => {
|
||||
testServer = await startTestServer({ port: TEST_PORT });
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
});
|
||||
|
||||
tap.test('Abrupt Disconnection - should handle socket destruction without QUIT', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
try {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once('connect', () => resolve());
|
||||
socket.once('error', reject);
|
||||
});
|
||||
|
||||
// Get banner
|
||||
const banner = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(banner).toInclude('220');
|
||||
|
||||
// Send EHLO
|
||||
socket.write('EHLO testhost\r\n');
|
||||
|
||||
await new Promise<string>((resolve) => {
|
||||
let data = '';
|
||||
const handler = (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
||||
socket.removeListener('data', handler);
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
socket.on('data', handler);
|
||||
});
|
||||
|
||||
// Abruptly disconnect without QUIT
|
||||
console.log('Destroying socket without QUIT...');
|
||||
socket.destroy();
|
||||
|
||||
// Wait a moment for server to handle the disconnection
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Test server recovery - try new connection
|
||||
console.log('Testing server recovery with new connection...');
|
||||
const recoverySocket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
const recoveryConnected = await new Promise<boolean>((resolve) => {
|
||||
recoverySocket.once('connect', () => resolve(true));
|
||||
recoverySocket.once('error', () => resolve(false));
|
||||
setTimeout(() => resolve(false), 5000);
|
||||
});
|
||||
|
||||
expect(recoveryConnected).toEqual(true);
|
||||
|
||||
if (recoveryConnected) {
|
||||
// Get banner from recovery connection
|
||||
const recoveryBanner = await new Promise<string>((resolve) => {
|
||||
recoverySocket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(recoveryBanner).toInclude('220');
|
||||
console.log('Server recovered successfully, accepting new connections');
|
||||
|
||||
// Clean up recovery connection properly
|
||||
recoverySocket.write('QUIT\r\n');
|
||||
recoverySocket.end();
|
||||
}
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Abrupt Disconnection - should handle multiple simultaneous abrupt disconnections', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
try {
|
||||
const connections = 5;
|
||||
const sockets: net.Socket[] = [];
|
||||
|
||||
// Create multiple connections
|
||||
for (let i = 0; i < connections; i++) {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once('connect', () => resolve());
|
||||
socket.once('error', reject);
|
||||
});
|
||||
|
||||
// Get banner
|
||||
await new Promise<void>((resolve) => {
|
||||
socket.once('data', () => resolve());
|
||||
});
|
||||
|
||||
sockets.push(socket);
|
||||
}
|
||||
|
||||
console.log(`Created ${connections} connections`);
|
||||
|
||||
// Abruptly disconnect all at once
|
||||
console.log('Destroying all sockets simultaneously...');
|
||||
sockets.forEach(socket => socket.destroy());
|
||||
|
||||
// Wait for server to handle disconnections
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
// Test that server still accepts new connections
|
||||
console.log('Testing server stability after multiple abrupt disconnections...');
|
||||
const testSocket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
const stillAccepting = await new Promise<boolean>((resolve) => {
|
||||
testSocket.once('connect', () => resolve(true));
|
||||
testSocket.once('error', () => resolve(false));
|
||||
setTimeout(() => resolve(false), 5000);
|
||||
});
|
||||
|
||||
expect(stillAccepting).toEqual(true);
|
||||
|
||||
if (stillAccepting) {
|
||||
const banner = await new Promise<string>((resolve) => {
|
||||
testSocket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(banner).toInclude('220');
|
||||
console.log('Server remained stable after multiple abrupt disconnections');
|
||||
|
||||
testSocket.write('QUIT\r\n');
|
||||
testSocket.end();
|
||||
}
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Abrupt Disconnection - should handle disconnection during DATA transfer', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
try {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once('connect', () => resolve());
|
||||
socket.once('error', reject);
|
||||
});
|
||||
|
||||
// Get banner
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Send EHLO
|
||||
socket.write('EHLO testhost\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
let data = '';
|
||||
const handler = (chunk: Buffer) => {
|
||||
data += chunk.toString();
|
||||
if (data.includes('\r\n') && (data.match(/^250 /m) || data.match(/^250-.*\r\n250 /ms))) {
|
||||
socket.removeListener('data', handler);
|
||||
resolve(data);
|
||||
}
|
||||
};
|
||||
socket.on('data', handler);
|
||||
});
|
||||
|
||||
// Send MAIL FROM
|
||||
socket.write('MAIL FROM:<sender@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Send RCPT TO
|
||||
socket.write('RCPT TO:<recipient@example.com>\r\n');
|
||||
await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
// Start DATA
|
||||
socket.write('DATA\r\n');
|
||||
const dataResponse = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(dataResponse).toInclude('354');
|
||||
|
||||
// Send partial email data then disconnect abruptly
|
||||
socket.write('From: sender@example.com\r\n');
|
||||
socket.write('To: recipient@example.com\r\n');
|
||||
socket.write('Subject: Test ');
|
||||
|
||||
console.log('Disconnecting during DATA transfer...');
|
||||
socket.destroy();
|
||||
|
||||
// Wait for server to handle disconnection
|
||||
await new Promise(resolve => setTimeout(resolve, 1500));
|
||||
|
||||
// Verify server can handle new connections
|
||||
const newSocket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
const canConnect = await new Promise<boolean>((resolve) => {
|
||||
newSocket.once('connect', () => resolve(true));
|
||||
newSocket.once('error', () => resolve(false));
|
||||
setTimeout(() => resolve(false), 5000);
|
||||
});
|
||||
|
||||
expect(canConnect).toEqual(true);
|
||||
|
||||
if (canConnect) {
|
||||
const banner = await new Promise<string>((resolve) => {
|
||||
newSocket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(banner).toInclude('220');
|
||||
console.log('Server recovered from disconnection during DATA transfer');
|
||||
|
||||
newSocket.write('QUIT\r\n');
|
||||
newSocket.end();
|
||||
}
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('Abrupt Disconnection - should timeout idle connections', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
try {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
socket.once('connect', () => resolve());
|
||||
socket.once('error', reject);
|
||||
});
|
||||
|
||||
// Get banner
|
||||
const banner = await new Promise<string>((resolve) => {
|
||||
socket.once('data', (chunk) => resolve(chunk.toString()));
|
||||
});
|
||||
|
||||
expect(banner).toInclude('220');
|
||||
console.log('Connected, now testing idle timeout...');
|
||||
|
||||
// Don't send any commands and wait for server to potentially timeout
|
||||
// Most servers have a timeout of 5-10 minutes, so we'll test shorter
|
||||
let disconnectedByServer = false;
|
||||
|
||||
socket.on('close', () => {
|
||||
disconnectedByServer = true;
|
||||
});
|
||||
|
||||
socket.on('end', () => {
|
||||
disconnectedByServer = true;
|
||||
});
|
||||
|
||||
// Wait 10 seconds to see if server has a short idle timeout
|
||||
await new Promise(resolve => setTimeout(resolve, 10000));
|
||||
|
||||
if (!disconnectedByServer) {
|
||||
console.log('Server maintains idle connections (no short timeout detected)');
|
||||
// Send QUIT to close gracefully
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
} else {
|
||||
console.log('Server disconnected idle connection');
|
||||
}
|
||||
|
||||
// Either behavior is acceptable
|
||||
expect(true).toEqual(true);
|
||||
|
||||
} finally {
|
||||
done.resolve();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
expect(true).toEqual(true);
|
||||
});
|
||||
|
||||
tap.start();
|
Reference in New Issue
Block a user