update
This commit is contained in:
374
test/suite/smtpserver_connection/test.cm-04.connection-limits.ts
Normal file
374
test/suite/smtpserver_connection/test.cm-04.connection-limits.ts
Normal file
@@ -0,0 +1,374 @@
|
||||
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';
|
||||
// Test configuration
|
||||
const TEST_PORT = 2525;
|
||||
const TEST_TIMEOUT = 5000;
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
// Setup
|
||||
tap.test('setup - start SMTP server', async () => {
|
||||
testServer = await startTestServer({ port: TEST_PORT });
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
});
|
||||
|
||||
// Test: Basic connection limit enforcement
|
||||
tap.test('Connection Limits - should handle multiple connections gracefully', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const maxConnections = 20; // Test with reasonable number
|
||||
const testConnections = maxConnections + 5; // Try 5 more than limit
|
||||
const connections: net.Socket[] = [];
|
||||
const connectionPromises: Promise<{ index: number; success: boolean; error?: string }>[] = [];
|
||||
|
||||
// Helper to create a connection with index
|
||||
const createConnectionWithIndex = (index: number): Promise<{ index: number; success: boolean; error?: string }> => {
|
||||
return new Promise((resolve) => {
|
||||
let timeoutHandle: NodeJS.Timeout;
|
||||
|
||||
try {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
clearTimeout(timeoutHandle);
|
||||
connections[index] = socket;
|
||||
|
||||
// Wait for server greeting
|
||||
socket.on('data', (data) => {
|
||||
if (data.toString().includes('220')) {
|
||||
resolve({ index, success: true });
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
clearTimeout(timeoutHandle);
|
||||
resolve({ index, success: false, error: err.message });
|
||||
});
|
||||
|
||||
timeoutHandle = setTimeout(() => {
|
||||
socket.destroy();
|
||||
resolve({ index, success: false, error: 'Connection timeout' });
|
||||
}, TEST_TIMEOUT);
|
||||
} catch (err: any) {
|
||||
resolve({ index, success: false, error: err.message });
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Create connections
|
||||
for (let i = 0; i < testConnections; i++) {
|
||||
connectionPromises.push(createConnectionWithIndex(i));
|
||||
}
|
||||
|
||||
const results = await Promise.all(connectionPromises);
|
||||
|
||||
// Count successful connections
|
||||
const successfulConnections = results.filter(r => r.success).length;
|
||||
const failedConnections = results.filter(r => !r.success).length;
|
||||
|
||||
// Clean up connections
|
||||
for (const socket of connections) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => socket.destroy(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Verify results
|
||||
expect(successfulConnections).toBeGreaterThan(0);
|
||||
|
||||
// If some connections were rejected, that's good (limit enforced)
|
||||
// If all connections succeeded, that's also acceptable (high/no limit)
|
||||
if (failedConnections > 0) {
|
||||
console.log(`Server enforced connection limit: ${successfulConnections} accepted, ${failedConnections} rejected`);
|
||||
} else {
|
||||
console.log(`Server accepted all ${successfulConnections} connections`);
|
||||
}
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Connection limit recovery
|
||||
tap.test('Connection Limits - should accept new connections after closing old ones', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const batchSize = 10;
|
||||
const firstBatch: net.Socket[] = [];
|
||||
const secondBatch: net.Socket[] = [];
|
||||
|
||||
// Create first batch of connections
|
||||
const firstBatchPromises = [];
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
firstBatchPromises.push(
|
||||
new Promise<boolean>((resolve) => {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
firstBatch.push(socket);
|
||||
socket.on('data', (data) => {
|
||||
if (data.toString().includes('220')) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', () => resolve(false));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const firstResults = await Promise.all(firstBatchPromises);
|
||||
const firstSuccessCount = firstResults.filter(r => r).length;
|
||||
|
||||
// Close first batch
|
||||
for (const socket of firstBatch) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.write('QUIT\r\n');
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for connections to close
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Destroy sockets
|
||||
for (const socket of firstBatch) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Create second batch
|
||||
const secondBatchPromises = [];
|
||||
for (let i = 0; i < batchSize; i++) {
|
||||
secondBatchPromises.push(
|
||||
new Promise<boolean>((resolve) => {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
secondBatch.push(socket);
|
||||
socket.on('data', (data) => {
|
||||
if (data.toString().includes('220')) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', () => resolve(false));
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const secondResults = await Promise.all(secondBatchPromises);
|
||||
const secondSuccessCount = secondResults.filter(r => r).length;
|
||||
|
||||
// Clean up second batch
|
||||
for (const socket of secondBatch) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => socket.destroy(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Both batches should have successful connections
|
||||
expect(firstSuccessCount).toBeGreaterThan(0);
|
||||
expect(secondSuccessCount).toBeGreaterThan(0);
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Rapid connection attempts
|
||||
tap.test('Connection Limits - should handle rapid connection attempts', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const rapidConnections = 50;
|
||||
const connections: net.Socket[] = [];
|
||||
let successCount = 0;
|
||||
let errorCount = 0;
|
||||
|
||||
// Create connections as fast as possible
|
||||
for (let i = 0; i < rapidConnections; i++) {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
connections.push(socket);
|
||||
successCount++;
|
||||
});
|
||||
|
||||
socket.on('error', () => {
|
||||
errorCount++;
|
||||
});
|
||||
}
|
||||
|
||||
// Wait for all connection attempts to settle
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Clean up
|
||||
for (const socket of connections) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Should handle at least some connections
|
||||
expect(successCount).toBeGreaterThan(0);
|
||||
console.log(`Rapid connections: ${successCount} succeeded, ${errorCount} failed`);
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Connection limit with different client IPs (simulated)
|
||||
tap.test('Connection Limits - should track connections per IP or globally', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
// Note: In real test, this would use different source IPs
|
||||
// For now, we test from same IP but document the behavior
|
||||
const connectionsPerIP = 5;
|
||||
const connections: net.Socket[] = [];
|
||||
const results: boolean[] = [];
|
||||
|
||||
for (let i = 0; i < connectionsPerIP; i++) {
|
||||
const result = await new Promise<boolean>((resolve) => {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: TEST_TIMEOUT
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
connections.push(socket);
|
||||
socket.on('data', (data) => {
|
||||
if (data.toString().includes('220')) {
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', () => resolve(false));
|
||||
});
|
||||
|
||||
results.push(result);
|
||||
}
|
||||
|
||||
const successCount = results.filter(r => r).length;
|
||||
|
||||
// Clean up
|
||||
for (const socket of connections) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.write('QUIT\r\n');
|
||||
setTimeout(() => socket.destroy(), 100);
|
||||
}
|
||||
}
|
||||
|
||||
// Should accept connections from same IP
|
||||
expect(successCount).toBeGreaterThan(0);
|
||||
console.log(`Per-IP connections: ${successCount} of ${connectionsPerIP} succeeded`);
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Test: Connection limit error messages
|
||||
tap.test('Connection Limits - should provide meaningful error when limit reached', async (tools) => {
|
||||
const done = tools.defer();
|
||||
|
||||
const manyConnections = 100;
|
||||
const connections: net.Socket[] = [];
|
||||
const errors: string[] = [];
|
||||
let rejected = false;
|
||||
|
||||
// Create many connections to try to hit limit
|
||||
const promises = [];
|
||||
for (let i = 0; i < manyConnections; i++) {
|
||||
promises.push(
|
||||
new Promise<void>((resolve) => {
|
||||
const socket = net.createConnection({
|
||||
host: 'localhost',
|
||||
port: TEST_PORT,
|
||||
timeout: 1000
|
||||
});
|
||||
|
||||
socket.on('connect', () => {
|
||||
connections.push(socket);
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const response = data.toString();
|
||||
// Check if server sends connection limit message
|
||||
if (response.includes('421') || response.includes('too many connections')) {
|
||||
rejected = true;
|
||||
errors.push(response);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
if (err.message.includes('ECONNREFUSED') || err.message.includes('ECONNRESET')) {
|
||||
rejected = true;
|
||||
errors.push(err.message);
|
||||
}
|
||||
resolve();
|
||||
});
|
||||
|
||||
socket.on('timeout', () => {
|
||||
resolve();
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
// Clean up
|
||||
for (const socket of connections) {
|
||||
if (socket && !socket.destroyed) {
|
||||
socket.destroy();
|
||||
}
|
||||
}
|
||||
|
||||
// Log results
|
||||
console.log(`Connection limit test: ${connections.length} connected, ${errors.length} rejected`);
|
||||
if (rejected) {
|
||||
console.log(`Sample rejection: ${errors[0]}`);
|
||||
}
|
||||
|
||||
// Should have handled connections (either accepted or properly rejected)
|
||||
expect(connections.length + errors.length).toBeGreaterThan(0);
|
||||
|
||||
done.resolve();
|
||||
|
||||
await done.promise;
|
||||
});
|
||||
|
||||
// Teardown
|
||||
tap.test('teardown - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
// Start the test
|
||||
tap.start();
|
Reference in New Issue
Block a user