This commit is contained in:
2025-05-23 19:03:44 +00:00
parent 7d28d23bbd
commit 1b141ec8f3
101 changed files with 30736 additions and 374 deletions

View File

@ -0,0 +1,406 @@
import * as plugins from '@push.rocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../server.loader.js';
const TEST_PORT = 2525;
interface DnsTestResult {
scenario: string;
domain: string;
expectedBehavior: string;
mailFromSuccess: boolean;
rcptToSuccess: boolean;
mailFromResponse: string;
rcptToResponse: string;
handledGracefully: boolean;
}
tap.test('prepare server', async () => {
await startTestServer();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-05: DNS resolution failure handling - Non-existent domains', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO dns-test\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
console.log('Testing DNS resolution for non-existent domains...');
// Test 1: Non-existent domain in MAIL FROM
socket.write('MAIL FROM:<sender@non-existent-domain-12345.invalid>\r\n');
const mailResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' MAIL FROM response:', mailResponse.trim());
// Server should either accept (and defer later) or reject immediately
const mailFromHandled = mailResponse.includes('250') ||
mailResponse.includes('450') ||
mailResponse.includes('550');
expect(mailFromHandled).toBeTrue();
// Reset if needed
if (mailResponse.includes('250')) {
socket.write('RSET\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
}
// Test 2: Non-existent domain in RCPT TO
socket.write('MAIL FROM:<sender@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket.write('RCPT TO:<recipient@non-existent-domain-xyz.invalid>\r\n');
const rcptResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' RCPT TO response:', rcptResponse.trim());
// Server should reject or defer non-existent domains
const rcptToHandled = rcptResponse.includes('450') || // Temporary failure
rcptResponse.includes('550') || // Permanent failure
rcptResponse.includes('553'); // Address error
expect(rcptToHandled).toBeTrue();
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-05: DNS resolution failure handling - Malformed domains', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO malformed-test\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
console.log('\nTesting malformed domain handling...');
const malformedDomains = [
'malformed..domain..test',
'invalid-.domain.com',
'domain.with.spaces .com',
'.leading-dot.com',
'trailing-dot.com.',
'domain@with@at.com',
'a'.repeat(255) + '.toolong.com' // Domain too long
];
for (const domain of malformedDomains) {
console.log(` Testing: ${domain.substring(0, 50)}${domain.length > 50 ? '...' : ''}`);
socket.write(`MAIL FROM:<test@${domain}>\r\n`);
const response = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
// Server should reject malformed domains
const properlyHandled = response.includes('501') || // Syntax error
response.includes('550') || // Rejected
response.includes('553'); // Address error
console.log(` Response: ${response.trim().substring(0, 50)}`);
expect(properlyHandled).toBeTrue();
// Reset if needed
if (!response.includes('5')) {
socket.write('RSET\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
}
}
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-05: DNS resolution failure handling - Special cases', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO special-test\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
console.log('\nTesting special DNS cases...');
// Test 1: Localhost (should work)
socket.write('MAIL FROM:<sender@localhost>\r\n');
const localhostResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' Localhost response:', localhostResponse.trim());
expect(localhostResponse).toInclude('250');
socket.write('RSET\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Test 2: IP address (should work)
socket.write('MAIL FROM:<sender@[127.0.0.1]>\r\n');
const ipResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' IP address response:', ipResponse.trim());
const ipHandled = ipResponse.includes('250') || ipResponse.includes('501');
expect(ipHandled).toBeTrue();
socket.write('RSET\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Test 3: Empty domain
socket.write('MAIL FROM:<sender@>\r\n');
const emptyResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' Empty domain response:', emptyResponse.trim());
expect(emptyResponse).toMatch(/50[1-3]/); // Should reject
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-05: DNS resolution failure handling - Mixed valid/invalid recipients', async (tools) => {
const done = tools.defer();
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write('EHLO mixed-test\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
console.log('\nTesting mixed valid/invalid recipients...');
// Start transaction
socket.write('MAIL FROM:<sender@example.com>\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// Add valid recipient
socket.write('RCPT TO:<valid@example.com>\r\n');
const validRcptResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' Valid recipient:', validRcptResponse.trim());
expect(validRcptResponse).toInclude('250');
// Add invalid recipient
socket.write('RCPT TO:<invalid@non-existent-domain-abc.invalid>\r\n');
const invalidRcptResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
console.log(' Invalid recipient:', invalidRcptResponse.trim());
// Server should reject invalid domain but keep transaction alive
const invalidHandled = invalidRcptResponse.includes('450') ||
invalidRcptResponse.includes('550') ||
invalidRcptResponse.includes('553');
expect(invalidHandled).toBeTrue();
// Try to send data (should work if at least one valid recipient)
socket.write('DATA\r\n');
const dataResponse = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
if (dataResponse.includes('354')) {
socket.write('Subject: Mixed recipient test\r\n\r\nTest\r\n.\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
console.log(' Message accepted with valid recipient');
} else {
console.log(' Server rejected DATA (acceptable behavior)');
}
socket.write('QUIT\r\n');
socket.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer();
});
tap.start();

View File

@ -0,0 +1,407 @@
import * as plugins from '@push.rocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../server.loader.js';
const TEST_PORT = 2525;
const createConnection = async (): Promise<net.Socket> => {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 5000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
return socket;
};
const getResponse = (socket: net.Socket, commandName: string): Promise<string> => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`${commandName} response timeout`));
}, 3000);
socket.once('data', (chunk: Buffer) => {
clearTimeout(timeout);
resolve(chunk.toString());
});
});
};
const testBasicSmtpFlow = async (socket: net.Socket): Promise<boolean> => {
try {
// Read greeting
await getResponse(socket, 'GREETING');
// Send EHLO
socket.write('EHLO recovery-test\r\n');
const ehloResp = await getResponse(socket, 'EHLO');
if (!ehloResp.includes('250')) return false;
// Wait for complete EHLO response
if (ehloResp.includes('250-')) {
await new Promise(resolve => setTimeout(resolve, 100));
}
socket.write('MAIL FROM:<sender@example.com>\r\n');
const mailResp = await getResponse(socket, 'MAIL FROM');
if (!mailResp.includes('250')) return false;
socket.write('RCPT TO:<recipient@example.com>\r\n');
const rcptResp = await getResponse(socket, 'RCPT TO');
if (!rcptResp.includes('250')) return false;
socket.write('DATA\r\n');
const dataResp = await getResponse(socket, 'DATA');
if (!dataResp.includes('354')) return false;
const testEmail = [
'From: sender@example.com',
'To: recipient@example.com',
'Subject: Recovery Test Email',
'',
'This email tests server recovery.',
'.',
''
].join('\r\n');
socket.write(testEmail);
const finalResp = await getResponse(socket, 'EMAIL DATA');
socket.write('QUIT\r\n');
socket.end();
return finalResp.includes('250');
} catch (error) {
console.log('Basic SMTP flow error:', error);
return false;
}
};
tap.test('prepare server', async () => {
await startTestServer();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-04: Error recovery - Invalid command recovery', async (tools) => {
const done = tools.defer();
try {
console.log('Testing recovery from invalid commands...');
// Phase 1: Send invalid commands
const socket1 = await createConnection();
await getResponse(socket1, 'GREETING');
// Send multiple invalid commands
socket1.write('INVALID_COMMAND\r\n');
const response1 = await getResponse(socket1, 'INVALID');
expect(response1).toMatch(/50[0-3]/); // Should get error response
socket1.write('ANOTHER_INVALID\r\n');
const response2 = await getResponse(socket1, 'INVALID');
expect(response2).toMatch(/50[0-3]/);
socket1.write('YET_ANOTHER_BAD_CMD\r\n');
const response3 = await getResponse(socket1, 'INVALID');
expect(response3).toMatch(/50[0-3]/);
socket1.end();
// Phase 2: Test recovery - server should still work normally
await new Promise(resolve => setTimeout(resolve, 500));
const socket2 = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket2);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from invalid commands');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-04: Error recovery - Malformed data recovery', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting recovery from malformed data...');
// Phase 1: Send malformed data
const socket1 = await createConnection();
await getResponse(socket1, 'GREETING');
socket1.write('EHLO testhost\r\n');
let data = '';
await new Promise<void>((resolve) => {
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket1.removeListener('data', handleData);
resolve();
}
};
socket1.on('data', handleData);
});
// Send malformed MAIL FROM
socket1.write('MAIL FROM: invalid-format\r\n');
const response1 = await getResponse(socket1, 'MALFORMED');
expect(response1).toMatch(/50[0-3]/);
// Send malformed RCPT TO
socket1.write('RCPT TO: also-invalid\r\n');
const response2 = await getResponse(socket1, 'MALFORMED');
expect(response2).toMatch(/50[0-3]/);
// Send malformed DATA with binary
socket1.write('DATA\x00\x01\x02CORRUPTED\r\n');
const response3 = await getResponse(socket1, 'CORRUPTED');
expect(response3).toMatch(/50[0-3]/);
socket1.end();
// Phase 2: Test recovery
await new Promise(resolve => setTimeout(resolve, 500));
const socket2 = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket2);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from malformed data');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-04: Error recovery - Premature disconnection recovery', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting recovery from premature disconnection...');
// Phase 1: Create incomplete transactions
for (let i = 0; i < 3; i++) {
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('EHLO abrupt-test\r\n');
let data = '';
await new Promise<void>((resolve) => {
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
socket.write('MAIL FROM:<test@example.com>\r\n');
await getResponse(socket, 'MAIL FROM');
// Abruptly close connection during transaction
socket.destroy();
console.log(` Abruptly closed connection ${i + 1}`);
await new Promise(resolve => setTimeout(resolve, 200));
}
// Phase 2: Test recovery
await new Promise(resolve => setTimeout(resolve, 1000));
const socket2 = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket2);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from premature disconnections');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-04: Error recovery - Data corruption recovery', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting recovery from data corruption...');
const socket1 = await createConnection();
await getResponse(socket1, 'GREETING');
socket1.write('EHLO corruption-test\r\n');
let data = '';
await new Promise<void>((resolve) => {
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket1.removeListener('data', handleData);
resolve();
}
};
socket1.on('data', handleData);
});
socket1.write('MAIL FROM:<sender@example.com>\r\n');
await getResponse(socket1, 'MAIL FROM');
socket1.write('RCPT TO:<recipient@example.com>\r\n');
await getResponse(socket1, 'RCPT TO');
socket1.write('DATA\r\n');
const dataResp = await getResponse(socket1, 'DATA');
expect(dataResp).toInclude('354');
// Send corrupted email data with null bytes and invalid characters
socket1.write('From: test\r\n\0\0\0CORRUPTED DATA\xff\xfe\r\n');
socket1.write('Subject: \x01\x02\x03Invalid\r\n');
socket1.write('\r\n');
socket1.write('Body with \0null bytes\r\n');
socket1.write('.\r\n');
try {
const response = await getResponse(socket1, 'CORRUPTED DATA');
console.log(' Server response to corrupted data:', response.substring(0, 50));
} catch (error) {
console.log(' Server rejected corrupted data (expected)');
}
socket1.end();
// Phase 2: Test recovery
await new Promise(resolve => setTimeout(resolve, 1000));
const socket2 = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket2);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from data corruption');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-04: Error recovery - Connection flooding recovery', async (tools) => {
const done = tools.defer();
const connections: net.Socket[] = [];
try {
console.log('\nTesting recovery from connection flooding...');
// Phase 1: Create multiple rapid connections
console.log(' Creating 15 rapid connections...');
for (let i = 0; i < 15; i++) {
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 2000
});
connections.push(socket);
// Don't wait for connection to complete
await new Promise(resolve => setTimeout(resolve, 50));
} catch (error) {
// Some connections might fail - that's expected
console.log(` Connection ${i + 1} failed (expected during flooding)`);
}
}
console.log(` Created ${connections.length} connections`);
// Close all connections
connections.forEach(conn => {
try {
conn.destroy();
} catch (e) {
// Ignore errors
}
});
// Phase 2: Test recovery
console.log(' Waiting for server to recover...');
await new Promise(resolve => setTimeout(resolve, 3000));
const socket2 = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket2);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from connection flooding');
done.resolve();
} catch (error) {
connections.forEach(conn => conn.destroy());
done.reject(error);
}
});
tap.test('REL-04: Error recovery - Mixed error scenario', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting recovery from mixed error scenarios...');
// Create multiple error conditions simultaneously
const errorPromises = [];
// Invalid command connection
errorPromises.push((async () => {
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('TOTALLY_WRONG\r\n');
await getResponse(socket, 'WRONG');
socket.destroy();
})());
// Malformed data connection
errorPromises.push((async () => {
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('MAIL FROM:<<<invalid>>>\r\n');
try {
await getResponse(socket, 'INVALID');
} catch (e) {
// Expected
}
socket.destroy();
})());
// Abrupt disconnection
errorPromises.push((async () => {
const socket = await createConnection();
socket.destroy();
})());
// Wait for all errors to execute
await Promise.allSettled(errorPromises);
console.log(' All error scenarios executed');
// Test recovery
await new Promise(resolve => setTimeout(resolve, 2000));
const socket = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from mixed error scenarios');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer();
});
tap.start();

View File

@ -0,0 +1,342 @@
import * as plugins from '@push.rocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../server.loader.js';
const TEST_PORT = 2525;
tap.test('prepare server', async () => {
await startTestServer();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-01: Long-running operation - Continuous email sending', async (tools) => {
const done = tools.defer();
const testDuration = 30000; // 30 seconds
const operationInterval = 2000; // 2 seconds between operations
const startTime = Date.now();
const endTime = startTime + testDuration;
let operations = 0;
let successful = 0;
let errors = 0;
let connectionIssues = 0;
const operationResults: Array<{
operation: number;
success: boolean;
duration: number;
error?: string;
timestamp: number;
}> = [];
console.log(`Running long-duration test for ${testDuration/1000} seconds...`);
const performOperation = async (operationId: number): Promise<void> => {
const operationStart = Date.now();
operations++;
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 10000
});
const result = await new Promise<{ success: boolean; error?: string; connectionIssue?: boolean }>((resolve) => {
let step = 'connecting';
let receivedData = '';
const timeout = setTimeout(() => {
socket.destroy();
resolve({
success: false,
error: `Timeout in step ${step}`,
connectionIssue: true
});
}, 10000);
socket.on('connect', () => {
step = 'connected';
});
socket.on('data', (chunk) => {
receivedData += chunk.toString();
const lines = receivedData.split('\r\n');
for (const line of lines) {
if (!line.trim()) continue;
// Check for errors
if (line.match(/^[45]\d\d\s/)) {
clearTimeout(timeout);
socket.destroy();
resolve({
success: false,
error: `SMTP error in ${step}: ${line}`,
connectionIssue: false
});
return;
}
// Process responses
if (step === 'connected' && line.startsWith('220')) {
step = 'ehlo';
socket.write(`EHLO longrun-${operationId}\r\n`);
} else if (step === 'ehlo' && line.includes('250 ') && !line.includes('250-')) {
step = 'mail_from';
socket.write(`MAIL FROM:<sender${operationId}@example.com>\r\n`);
} else if (step === 'mail_from' && line.startsWith('250')) {
step = 'rcpt_to';
socket.write(`RCPT TO:<recipient${operationId}@example.com>\r\n`);
} else if (step === 'rcpt_to' && line.startsWith('250')) {
step = 'data';
socket.write('DATA\r\n');
} else if (step === 'data' && line.startsWith('354')) {
step = 'email_content';
const emailContent = [
`From: sender${operationId}@example.com`,
`To: recipient${operationId}@example.com`,
`Subject: Long Running Test Operation ${operationId}`,
`Date: ${new Date().toUTCString()}`,
'',
`This is test operation ${operationId} for long-running reliability testing.`,
`Timestamp: ${Date.now()}`,
'.',
''
].join('\r\n');
socket.write(emailContent);
} else if (step === 'email_content' && line.startsWith('250')) {
step = 'quit';
socket.write('QUIT\r\n');
} else if (step === 'quit' && line.startsWith('221')) {
clearTimeout(timeout);
socket.end();
resolve({
success: true
});
return;
}
}
});
socket.on('error', (error) => {
clearTimeout(timeout);
resolve({
success: false,
error: error.message,
connectionIssue: true
});
});
socket.on('close', () => {
if (step !== 'quit') {
clearTimeout(timeout);
resolve({
success: false,
error: 'Connection closed unexpectedly',
connectionIssue: true
});
}
});
});
const duration = Date.now() - operationStart;
if (result.success) {
successful++;
} else {
errors++;
if (result.connectionIssue) {
connectionIssues++;
}
}
operationResults.push({
operation: operationId,
success: result.success,
duration,
error: result.error,
timestamp: operationStart
});
} catch (error) {
errors++;
operationResults.push({
operation: operationId,
success: false,
duration: Date.now() - operationStart,
error: error instanceof Error ? error.message : 'Unknown error',
timestamp: operationStart
});
}
};
try {
// Run operations continuously until end time
while (Date.now() < endTime) {
const operationStart = Date.now();
await performOperation(operations + 1);
// Calculate wait time for next operation
const nextOperation = operationStart + operationInterval;
const waitTime = nextOperation - Date.now();
if (waitTime > 0 && Date.now() < endTime) {
await new Promise(resolve => setTimeout(resolve, waitTime));
}
// Progress update every 5 operations
if (operations % 5 === 0) {
console.log(`Progress: ${operations} operations, ${successful} successful, ${errors} errors`);
}
}
// Calculate results
const totalDuration = Date.now() - startTime;
const successRate = successful / operations;
const connectionIssueRate = connectionIssues / operations;
const avgOperationTime = operationResults.reduce((sum, r) => sum + r.duration, 0) / operations;
console.log(`\nLong-Running Operation Results:`);
console.log(`Total duration: ${(totalDuration/1000).toFixed(1)}s`);
console.log(`Total operations: ${operations}`);
console.log(`Successful: ${successful} (${(successRate * 100).toFixed(1)}%)`);
console.log(`Errors: ${errors}`);
console.log(`Connection issues: ${connectionIssues} (${(connectionIssueRate * 100).toFixed(1)}%)`);
console.log(`Average operation time: ${avgOperationTime.toFixed(0)}ms`);
// Show last few operations for debugging
console.log('\nLast 5 operations:');
operationResults.slice(-5).forEach(op => {
console.log(` Op ${op.operation}: ${op.success ? 'success' : 'failed'} (${op.duration}ms)${op.error ? ' - ' + op.error : ''}`);
});
// Test passes with 85% success rate and max 10% connection issues
expect(successRate).toBeGreaterThanOrEqual(0.85);
expect(connectionIssueRate).toBeLessThanOrEqual(0.1);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-01: Long-running operation - Server stability check', async (tools) => {
const done = tools.defer();
const checkDuration = 15000; // 15 seconds
const checkInterval = 3000; // 3 seconds between checks
const startTime = Date.now();
const endTime = startTime + checkDuration;
const stabilityChecks: Array<{
timestamp: number;
responseTime: number;
success: boolean;
error?: string;
}> = [];
console.log(`\nRunning server stability checks for ${checkDuration/1000} seconds...`);
try {
while (Date.now() < endTime) {
const checkStart = Date.now();
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 5000
});
const checkResult = await new Promise<{ success: boolean; responseTime: number; error?: string }>((resolve) => {
const connectTime = Date.now();
let greetingReceived = false;
const timeout = setTimeout(() => {
socket.destroy();
resolve({
success: false,
responseTime: Date.now() - connectTime,
error: 'Timeout waiting for greeting'
});
}, 5000);
socket.on('connect', () => {
// Connected
});
socket.once('data', (chunk) => {
const response = chunk.toString();
clearTimeout(timeout);
greetingReceived = true;
if (response.startsWith('220')) {
socket.write('QUIT\r\n');
socket.end();
resolve({
success: true,
responseTime: Date.now() - connectTime
});
} else {
socket.end();
resolve({
success: false,
responseTime: Date.now() - connectTime,
error: `Unexpected greeting: ${response.substring(0, 50)}`
});
}
});
socket.on('error', (error) => {
clearTimeout(timeout);
resolve({
success: false,
responseTime: Date.now() - connectTime,
error: error.message
});
});
});
stabilityChecks.push({
timestamp: checkStart,
responseTime: checkResult.responseTime,
success: checkResult.success,
error: checkResult.error
});
console.log(`Stability check ${stabilityChecks.length}: ${checkResult.success ? 'OK' : 'FAILED'} (${checkResult.responseTime}ms)`);
// Wait for next check
const nextCheck = checkStart + checkInterval;
const waitTime = nextCheck - Date.now();
if (waitTime > 0 && Date.now() < endTime) {
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
// Analyze stability
const successfulChecks = stabilityChecks.filter(c => c.success).length;
const avgResponseTime = stabilityChecks
.filter(c => c.success)
.reduce((sum, c) => sum + c.responseTime, 0) / successfulChecks || 0;
const maxResponseTime = Math.max(...stabilityChecks.filter(c => c.success).map(c => c.responseTime));
console.log(`\nStability Check Results:`);
console.log(`Total checks: ${stabilityChecks.length}`);
console.log(`Successful: ${successfulChecks} (${(successfulChecks/stabilityChecks.length * 100).toFixed(1)}%)`);
console.log(`Average response time: ${avgResponseTime.toFixed(0)}ms`);
console.log(`Max response time: ${maxResponseTime}ms`);
// All checks should succeed for stable server
expect(successfulChecks).toBe(stabilityChecks.length);
expect(avgResponseTime).toBeLessThan(1000);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer();
});
tap.start();

View File

@ -0,0 +1,416 @@
import * as plugins from '@push.rocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../server.loader.js';
const TEST_PORT = 2525;
const createConnection = async (): Promise<net.Socket> => {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 5000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
return socket;
};
const getResponse = (socket: net.Socket, commandName: string): Promise<string> => {
return new Promise((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error(`${commandName} response timeout`));
}, 3000);
socket.once('data', (chunk: Buffer) => {
clearTimeout(timeout);
resolve(chunk.toString());
});
});
};
const testBasicSmtpFlow = async (socket: net.Socket): Promise<boolean> => {
try {
await getResponse(socket, 'GREETING');
socket.write('EHLO test.example.com\r\n');
const ehloResp = await getResponse(socket, 'EHLO');
if (!ehloResp.includes('250')) return false;
// Wait for complete EHLO response
if (ehloResp.includes('250-')) {
await new Promise(resolve => setTimeout(resolve, 100));
}
socket.write('MAIL FROM:<test@example.com>\r\n');
const mailResp = await getResponse(socket, 'MAIL FROM');
if (!mailResp.includes('250')) return false;
socket.write('RCPT TO:<recipient@example.com>\r\n');
const rcptResp = await getResponse(socket, 'RCPT TO');
if (!rcptResp.includes('250')) return false;
socket.write('DATA\r\n');
const dataResp = await getResponse(socket, 'DATA');
if (!dataResp.includes('354')) return false;
const testEmail = [
'From: test@example.com',
'To: recipient@example.com',
'Subject: Interruption Recovery Test',
'',
'This email tests server recovery after network interruption.',
'.',
''
].join('\r\n');
socket.write(testEmail);
const finalResp = await getResponse(socket, 'EMAIL DATA');
socket.write('QUIT\r\n');
socket.end();
return finalResp.includes('250');
} catch (error) {
return false;
}
};
tap.test('prepare server', async () => {
await startTestServer();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-06: Network interruption - Sudden connection drop', async (tools) => {
const done = tools.defer();
try {
console.log('Testing sudden connection drop during session...');
// Phase 1: Create connection and drop it mid-session
const socket1 = await createConnection();
await getResponse(socket1, 'GREETING');
socket1.write('EHLO testhost\r\n');
let data = '';
await new Promise<void>((resolve) => {
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket1.removeListener('data', handleData);
resolve();
}
};
socket1.on('data', handleData);
});
socket1.write('MAIL FROM:<sender@example.com>\r\n');
await getResponse(socket1, 'MAIL FROM');
// Abruptly close connection during active session
socket1.destroy();
console.log(' Connection abruptly closed');
// Phase 2: Test recovery
await new Promise(resolve => setTimeout(resolve, 1000));
const socket2 = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(socket2);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from sudden connection drop');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-06: Network interruption - Data transfer interruption', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting connection interruption during data transfer...');
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('EHLO datatest\r\n');
let data = '';
await new Promise<void>((resolve) => {
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
socket.write('MAIL FROM:<sender@example.com>\r\n');
await getResponse(socket, 'MAIL FROM');
socket.write('RCPT TO:<recipient@example.com>\r\n');
await getResponse(socket, 'RCPT TO');
socket.write('DATA\r\n');
const dataResp = await getResponse(socket, 'DATA');
expect(dataResp).toInclude('354');
// Start sending data but interrupt midway
socket.write('From: sender@example.com\r\n');
socket.write('To: recipient@example.com\r\n');
socket.write('Subject: Interruption Test\r\n\r\n');
socket.write('This email will be interrupted...\r\n');
// Wait briefly then destroy connection (simulating network loss)
await new Promise(resolve => setTimeout(resolve, 500));
socket.destroy();
console.log(' Connection interrupted during data transfer');
// Test recovery
await new Promise(resolve => setTimeout(resolve, 1500));
const newSocket = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(newSocket);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from data transfer interruption');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-06: Network interruption - Rapid reconnection attempts', async (tools) => {
const done = tools.defer();
const connections: net.Socket[] = [];
try {
console.log('\nTesting rapid reconnection after interruptions...');
// Create and immediately destroy multiple connections
console.log(' Creating 5 unstable connections...');
for (let i = 0; i < 5; i++) {
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 2000
});
connections.push(socket);
// Destroy after short random delay to simulate instability
setTimeout(() => socket.destroy(), 50 + Math.random() * 150);
await new Promise(resolve => setTimeout(resolve, 50));
} catch (error) {
// Expected - some connections might fail
}
}
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 2000));
// Now test if server can handle normal connections
let successfulConnections = 0;
console.log(' Testing recovery with stable connections...');
for (let i = 0; i < 3; i++) {
try {
const socket = await createConnection();
const success = await testBasicSmtpFlow(socket);
if (success) {
successfulConnections++;
}
} catch (error) {
console.log(` Connection ${i + 1} failed:`, error.message);
}
await new Promise(resolve => setTimeout(resolve, 500));
}
const recoveryRate = successfulConnections / 3;
console.log(` Recovery rate: ${successfulConnections}/3 (${(recoveryRate * 100).toFixed(0)}%)`);
expect(recoveryRate).toBeGreaterThanOrEqual(0.66); // At least 2/3 should succeed
console.log('✓ Server recovered from rapid reconnection attempts');
done.resolve();
} catch (error) {
connections.forEach(conn => conn.destroy());
done.reject(error);
}
});
tap.test('REL-06: Network interruption - Partial command interruption', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting partial command transmission interruption...');
const socket = await createConnection();
await getResponse(socket, 'GREETING');
// Send partial EHLO command and interrupt
socket.write('EH');
console.log(' Sent partial command "EH"');
await new Promise(resolve => setTimeout(resolve, 100));
socket.destroy();
console.log(' Connection destroyed with incomplete command');
// Test recovery
await new Promise(resolve => setTimeout(resolve, 1000));
const newSocket = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(newSocket);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered from partial command interruption');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-06: Network interruption - Multiple interruption types', async (tools) => {
const done = tools.defer();
const results: Array<{ type: string; recovered: boolean }> = [];
try {
console.log('\nTesting recovery from multiple interruption types...');
// Test 1: Interrupt after greeting
try {
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.destroy();
results.push({ type: 'after-greeting', recovered: false });
} catch (e) {
results.push({ type: 'after-greeting', recovered: false });
}
await new Promise(resolve => setTimeout(resolve, 500));
// Test 2: Interrupt during EHLO
try {
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('EHLO te');
socket.destroy();
results.push({ type: 'during-ehlo', recovered: false });
} catch (e) {
results.push({ type: 'during-ehlo', recovered: false });
}
await new Promise(resolve => setTimeout(resolve, 500));
// Test 3: Interrupt with invalid data
try {
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('\x00\x01\x02\x03');
socket.destroy();
results.push({ type: 'invalid-data', recovered: false });
} catch (e) {
results.push({ type: 'invalid-data', recovered: false });
}
await new Promise(resolve => setTimeout(resolve, 1000));
// Test final recovery
try {
const socket = await createConnection();
const success = await testBasicSmtpFlow(socket);
if (success) {
// Mark all previous tests as recovered
results.forEach(r => r.recovered = true);
}
} catch (error) {
console.log('Final recovery failed:', error.message);
}
const recoveredCount = results.filter(r => r.recovered).length;
console.log(`\nInterruption recovery summary:`);
results.forEach(r => {
console.log(` ${r.type}: ${r.recovered ? 'recovered' : 'failed'}`);
});
expect(recoveredCount).toBeGreaterThan(0);
console.log(`✓ Server recovered from ${recoveredCount}/${results.length} interruption scenarios`);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-06: Network interruption - Long delay recovery', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting recovery after long network interruption...');
// Create connection and start transaction
const socket = await createConnection();
await getResponse(socket, 'GREETING');
socket.write('EHLO longdelay\r\n');
let data = '';
await new Promise<void>((resolve) => {
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
socket.write('MAIL FROM:<sender@example.com>\r\n');
await getResponse(socket, 'MAIL FROM');
// Simulate long network interruption
socket.pause();
console.log(' Connection paused (simulating network freeze)');
await new Promise(resolve => setTimeout(resolve, 5000)); // 5 second "freeze"
// Try to continue - should fail
socket.resume();
socket.write('RCPT TO:<recipient@example.com>\r\n');
let continuationFailed = false;
try {
await getResponse(socket, 'RCPT TO');
} catch (error) {
continuationFailed = true;
console.log(' Continuation failed as expected');
}
socket.destroy();
// Test recovery with new connection
const newSocket = await createConnection();
const recoverySuccess = await testBasicSmtpFlow(newSocket);
expect(recoverySuccess).toBeTrue();
console.log('✓ Server recovered after long network interruption');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer();
});
tap.start();

View File

@ -0,0 +1,395 @@
import * as plugins from '@push.rocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../server.loader.js';
const TEST_PORT = 2525;
interface ResourceMetrics {
timestamp: number;
memoryUsage: {
rss: number;
heapTotal: number;
heapUsed: number;
external: number;
};
processInfo: {
pid: number;
uptime: number;
cpuUsage: NodeJS.CpuUsage;
};
}
interface LeakAnalysis {
memoryGrowthMB: number;
memoryTrend: number;
stabilityScore: number;
memoryLeakDetected: boolean;
resourcesStable: boolean;
samplesAnalyzed: number;
initialMemoryMB: number;
finalMemoryMB: number;
}
const captureResourceMetrics = async (): Promise<ResourceMetrics> => {
// Force GC if available before measurement
if (global.gc) {
global.gc();
await new Promise(resolve => setTimeout(resolve, 100));
}
const memUsage = process.memoryUsage();
return {
timestamp: Date.now(),
memoryUsage: {
rss: Math.round(memUsage.rss / 1024 / 1024 * 100) / 100,
heapTotal: Math.round(memUsage.heapTotal / 1024 / 1024 * 100) / 100,
heapUsed: Math.round(memUsage.heapUsed / 1024 / 1024 * 100) / 100,
external: Math.round(memUsage.external / 1024 / 1024 * 100) / 100
},
processInfo: {
pid: process.pid,
uptime: process.uptime(),
cpuUsage: process.cpuUsage()
}
};
};
const analyzeResourceLeaks = (initial: ResourceMetrics, samples: Array<{ operation: number; metrics: ResourceMetrics }>, final: ResourceMetrics): LeakAnalysis => {
const memoryGrowthMB = final.memoryUsage.heapUsed - initial.memoryUsage.heapUsed;
// Analyze memory trend over samples
let memoryTrend = 0;
if (samples.length > 1) {
const firstSample = samples[0].metrics.memoryUsage.heapUsed;
const lastSample = samples[samples.length - 1].metrics.memoryUsage.heapUsed;
memoryTrend = lastSample - firstSample;
}
// Calculate stability score based on memory variance
let stabilityScore = 1.0;
if (samples.length > 2) {
const memoryValues = samples.map(s => s.metrics.memoryUsage.heapUsed);
const average = memoryValues.reduce((a, b) => a + b, 0) / memoryValues.length;
const variance = memoryValues.reduce((acc, val) => acc + Math.pow(val - average, 2), 0) / memoryValues.length;
const stdDev = Math.sqrt(variance);
stabilityScore = Math.max(0, 1 - (stdDev / average));
}
return {
memoryGrowthMB: Math.round(memoryGrowthMB * 100) / 100,
memoryTrend: Math.round(memoryTrend * 100) / 100,
stabilityScore: Math.round(stabilityScore * 100) / 100,
memoryLeakDetected: memoryGrowthMB > 50,
resourcesStable: stabilityScore > 0.8 && memoryGrowthMB < 25,
samplesAnalyzed: samples.length,
initialMemoryMB: initial.memoryUsage.heapUsed,
finalMemoryMB: final.memoryUsage.heapUsed
};
};
tap.test('prepare server', async () => {
await startTestServer();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-03: Resource leak detection - Memory leak analysis', async (tools) => {
const done = tools.defer();
const operationCount = 20;
const connections: net.Socket[] = [];
const samples: Array<{ operation: number; metrics: ResourceMetrics }> = [];
try {
const initialMetrics = await captureResourceMetrics();
console.log(`📊 Initial memory: ${initialMetrics.memoryUsage.heapUsed}MB`);
for (let i = 0; i < operationCount; i++) {
console.log(`🔄 Operation ${i + 1}/${operationCount}...`);
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
connections.push(socket);
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Send EHLO
socket.write(`EHLO leaktest-${i}\r\n`);
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket.removeListener('data', handleData);
resolve();
}
};
socket.on('data', handleData);
});
// Complete email transaction
socket.write(`MAIL FROM:<sender${i}@example.com>\r\n`);
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket.write(`RCPT TO:<recipient${i}@example.com>\r\n`);
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket.write('DATA\r\n');
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('354');
resolve();
});
});
const emailContent = [
`From: sender${i}@example.com`,
`To: recipient${i}@example.com`,
`Subject: Resource Leak Test ${i + 1}`,
`Message-ID: <leak-test-${i}-${Date.now()}@example.com>`,
'',
`This is resource leak test iteration ${i + 1}.`,
'.',
''
].join('\r\n');
socket.write(emailContent);
await new Promise<void>((resolve) => {
socket.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket.write('QUIT\r\n');
await new Promise<void>((resolve) => {
socket.once('data', () => {
socket.end();
resolve();
});
});
// Capture metrics every 5 operations
if ((i + 1) % 5 === 0) {
const metrics = await captureResourceMetrics();
samples.push({
operation: i + 1,
metrics
});
console.log(`📈 Sample ${samples.length}: Memory ${metrics.memoryUsage.heapUsed}MB`);
}
// Small delay between operations
await new Promise(resolve => setTimeout(resolve, 100));
}
// Clean up all connections
connections.forEach(conn => {
if (!conn.destroyed) {
conn.destroy();
}
});
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 2000));
const finalMetrics = await captureResourceMetrics();
const leakAnalysis = analyzeResourceLeaks(initialMetrics, samples, finalMetrics);
console.log('\n📊 Resource Leak Analysis:');
console.log(`Initial memory: ${leakAnalysis.initialMemoryMB}MB`);
console.log(`Final memory: ${leakAnalysis.finalMemoryMB}MB`);
console.log(`Memory growth: ${leakAnalysis.memoryGrowthMB}MB`);
console.log(`Memory trend: ${leakAnalysis.memoryTrend}MB`);
console.log(`Stability score: ${leakAnalysis.stabilityScore}`);
console.log(`Memory leak detected: ${leakAnalysis.memoryLeakDetected}`);
console.log(`Resources stable: ${leakAnalysis.resourcesStable}`);
expect(leakAnalysis.memoryLeakDetected).toBeFalse();
expect(leakAnalysis.resourcesStable).toBeTrue();
done.resolve();
} catch (error) {
connections.forEach(conn => conn.destroy());
done.reject(error);
}
});
tap.test('REL-03: Resource leak detection - Connection leak test', async (tools) => {
const done = tools.defer();
const abandonedConnections: net.Socket[] = [];
try {
console.log('\nTesting for connection resource leaks...');
// Create connections that are abandoned without proper cleanup
for (let i = 0; i < 10; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
abandonedConnections.push(socket);
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Read greeting but don't complete transaction
await new Promise<void>((resolve) => {
socket.once('data', () => resolve());
});
// Start but don't complete EHLO
socket.write(`EHLO abandoned-${i}\r\n`);
// Don't wait for response, just move to next
await new Promise(resolve => setTimeout(resolve, 50));
}
console.log('Created 10 abandoned connections');
// Wait a bit
await new Promise(resolve => setTimeout(resolve, 2000));
// Try to create new connections - should still work
let newConnectionsSuccessful = 0;
for (let i = 0; i < 5; i++) {
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 5000
});
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error('Connection timeout'));
}, 5000);
socket.once('connect', () => {
clearTimeout(timeout);
resolve();
});
socket.once('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
// Verify connection works
const greeting = await new Promise<string>((resolve) => {
socket.once('data', (chunk) => {
resolve(chunk.toString());
});
});
if (greeting.includes('220')) {
newConnectionsSuccessful++;
socket.write('QUIT\r\n');
socket.end();
}
} catch (error) {
console.log(`New connection ${i + 1} failed:`, error.message);
}
}
// Clean up abandoned connections
abandonedConnections.forEach(conn => conn.destroy());
console.log(`New connections successful: ${newConnectionsSuccessful}/5`);
// Server should still accept new connections despite abandoned ones
expect(newConnectionsSuccessful).toBeGreaterThanOrEqual(4);
done.resolve();
} catch (error) {
abandonedConnections.forEach(conn => conn.destroy());
done.reject(error);
}
});
tap.test('REL-03: Resource leak detection - Rapid create/destroy cycles', async (tools) => {
const done = tools.defer();
const cycles = 30;
const initialMetrics = await captureResourceMetrics();
try {
console.log('\nTesting rapid connection create/destroy cycles...');
for (let i = 0; i < cycles; i++) {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 5000
});
await new Promise<void>((resolve, reject) => {
socket.once('connect', resolve);
socket.once('error', reject);
});
// Immediately destroy after connect
socket.destroy();
// Very short delay
await new Promise(resolve => setTimeout(resolve, 20));
if ((i + 1) % 10 === 0) {
console.log(`Completed ${i + 1} cycles`);
}
}
// Wait for resources to be released
await new Promise(resolve => setTimeout(resolve, 3000));
const finalMetrics = await captureResourceMetrics();
const memoryGrowth = finalMetrics.memoryUsage.heapUsed - initialMetrics.memoryUsage.heapUsed;
console.log(`Memory growth after ${cycles} cycles: ${memoryGrowth.toFixed(2)}MB`);
// Memory growth should be minimal
expect(memoryGrowth).toBeLessThan(10);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer();
});
tap.start();

View File

@ -0,0 +1,402 @@
import * as plugins from '@push.rocks/tapbundle';
import { expect, tap } from '@push.rocks/tapbundle';
import * as net from 'net';
import { startTestServer, stopTestServer } from '../server.loader.js';
const TEST_PORT = 2525;
tap.test('prepare server', async () => {
await startTestServer();
await new Promise(resolve => setTimeout(resolve, 100));
});
tap.test('REL-02: Restart recovery - Server state after restart', async (tools) => {
const done = tools.defer();
try {
console.log('Testing server state and recovery capabilities...');
// First, establish that server is working normally
const socket1 = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket1.once('connect', resolve);
socket1.once('error', reject);
});
// Read greeting
const greeting1 = await new Promise<string>((resolve) => {
socket1.once('data', (chunk) => {
resolve(chunk.toString());
});
});
expect(greeting1).toInclude('220');
console.log('Initial connection successful');
// Send EHLO
socket1.write('EHLO testhost\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket1.removeListener('data', handleData);
resolve();
}
};
socket1.on('data', handleData);
});
// Complete a transaction
socket1.write('MAIL FROM:<sender@example.com>\r\n');
await new Promise<void>((resolve) => {
socket1.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket1.write('RCPT TO:<recipient@example.com>\r\n');
await new Promise<void>((resolve) => {
socket1.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket1.write('DATA\r\n');
await new Promise<void>((resolve) => {
socket1.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('354');
resolve();
});
});
const emailContent = [
'From: sender@example.com',
'To: recipient@example.com',
'Subject: Pre-restart test',
'',
'Testing server state before restart.',
'.',
''
].join('\r\n');
socket1.write(emailContent);
await new Promise<void>((resolve) => {
socket1.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket1.write('QUIT\r\n');
socket1.end();
console.log('Pre-restart transaction completed successfully');
// Simulate server restart by closing and reopening connections
console.log('\nSimulating server restart scenario...');
// Wait a moment to simulate restart time
await new Promise(resolve => setTimeout(resolve, 2000));
// Test recovery after simulated restart
const socket2 = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket2.once('connect', resolve);
socket2.once('error', reject);
});
// Read greeting after "restart"
const greeting2 = await new Promise<string>((resolve) => {
socket2.once('data', (chunk) => {
resolve(chunk.toString());
});
});
expect(greeting2).toInclude('220');
console.log('Post-restart connection successful');
// Verify server is fully functional after restart
socket2.write('EHLO testhost-postrestart\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket2.removeListener('data', handleData);
resolve();
}
};
socket2.on('data', handleData);
});
// Complete another transaction to verify full recovery
socket2.write('MAIL FROM:<sender2@example.com>\r\n');
await new Promise<void>((resolve) => {
socket2.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket2.write('RCPT TO:<recipient2@example.com>\r\n');
await new Promise<void>((resolve) => {
socket2.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket2.write('DATA\r\n');
await new Promise<void>((resolve) => {
socket2.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('354');
resolve();
});
});
const postRestartEmail = [
'From: sender2@example.com',
'To: recipient2@example.com',
'Subject: Post-restart recovery test',
'',
'Testing server recovery after restart.',
'.',
''
].join('\r\n');
socket2.write(postRestartEmail);
await new Promise<void>((resolve) => {
socket2.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
socket2.write('QUIT\r\n');
socket2.end();
console.log('Post-restart transaction completed successfully');
console.log('Server recovered successfully from restart');
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-02: Restart recovery - Multiple rapid reconnections', async (tools) => {
const done = tools.defer();
const rapidConnections = 10;
let successfulReconnects = 0;
try {
console.log(`\nTesting rapid reconnection after disruption (${rapidConnections} attempts)...`);
for (let i = 0; i < rapidConnections; i++) {
try {
const socket = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 5000
});
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
socket.destroy();
reject(new Error('Connection timeout'));
}, 5000);
socket.once('connect', () => {
clearTimeout(timeout);
resolve();
});
socket.once('error', (err) => {
clearTimeout(timeout);
reject(err);
});
});
// Read greeting
const greeting = await new Promise<string>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error('Greeting timeout'));
}, 3000);
socket.once('data', (chunk) => {
clearTimeout(timeout);
resolve(chunk.toString());
});
});
if (greeting.includes('220')) {
successfulReconnects++;
socket.write('QUIT\r\n');
socket.end();
} else {
socket.destroy();
}
// Very short delay between attempts
await new Promise(resolve => setTimeout(resolve, 100));
} catch (error) {
console.log(`Reconnection ${i + 1} failed:`, error.message);
}
}
const reconnectRate = successfulReconnects / rapidConnections;
console.log(`Successful reconnections: ${successfulReconnects}/${rapidConnections} (${(reconnectRate * 100).toFixed(1)}%)`);
// Expect high success rate for good recovery
expect(reconnectRate).toBeGreaterThanOrEqual(0.8);
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('REL-02: Restart recovery - State persistence check', async (tools) => {
const done = tools.defer();
try {
console.log('\nTesting server state persistence across connections...');
// Create initial connection and start transaction
const socket1 = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket1.once('connect', resolve);
socket1.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket1.once('data', () => resolve());
});
// Send EHLO
socket1.write('EHLO persistence-test\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket1.removeListener('data', handleData);
resolve();
}
};
socket1.on('data', handleData);
});
// Start transaction but don't complete it
socket1.write('MAIL FROM:<incomplete@example.com>\r\n');
await new Promise<void>((resolve) => {
socket1.once('data', (chunk) => {
const response = chunk.toString();
expect(response).toInclude('250');
resolve();
});
});
// Abruptly close connection
socket1.destroy();
console.log('Abruptly closed connection with incomplete transaction');
// Wait briefly
await new Promise(resolve => setTimeout(resolve, 1000));
// Create new connection and verify server recovered
const socket2 = net.createConnection({
host: 'localhost',
port: TEST_PORT,
timeout: 30000
});
await new Promise<void>((resolve, reject) => {
socket2.once('connect', resolve);
socket2.once('error', reject);
});
// Read greeting
await new Promise<void>((resolve) => {
socket2.once('data', () => resolve());
});
// Send EHLO
socket2.write('EHLO recovery-test\r\n');
await new Promise<void>((resolve) => {
let data = '';
const handleData = (chunk: Buffer) => {
data += chunk.toString();
if (data.includes('250 ') && !data.includes('250-')) {
socket2.removeListener('data', handleData);
resolve();
}
};
socket2.on('data', handleData);
});
// Try new transaction - should work without issues from previous incomplete one
socket2.write('MAIL FROM:<recovery@example.com>\r\n');
const mailResponse = await new Promise<string>((resolve) => {
socket2.once('data', (chunk) => {
resolve(chunk.toString());
});
});
expect(mailResponse).toInclude('250');
console.log('Server recovered successfully - new transaction started without issues');
socket2.write('QUIT\r\n');
socket2.end();
done.resolve();
} catch (error) {
done.reject(error);
}
});
tap.test('cleanup server', async () => {
await stopTestServer();
});
tap.start();