342 lines
11 KiB
TypeScript
342 lines
11 KiB
TypeScript
|
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();
|