dcrouter/test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
2025-05-26 14:50:55 +00:00

520 lines
18 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
tap.test('CREL-04: Basic Connection Recovery from Server Issues', async () => {
console.log('\n💥 Testing SMTP Client Connection Recovery');
console.log('=' .repeat(60));
console.log('\n🔌 Testing recovery from connection drops...');
let connectionCount = 0;
let dropConnections = false;
// Create test server that can simulate connection drops
const server = net.createServer(socket => {
connectionCount++;
console.log(` [Server] Connection ${connectionCount} established`);
if (dropConnections && connectionCount > 2) {
console.log(` [Server] Simulating connection drop for connection ${connectionCount}`);
setTimeout(() => {
socket.destroy();
}, 100);
return;
}
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server.address() as net.AddressInfo).port;
try {
console.log(' Creating SMTP client with connection recovery settings...');
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 2,
maxMessages: 50,
connectionTimeout: 2000
});
const emails = [];
for (let i = 0; i < 8; i++) {
emails.push(new Email({
from: 'sender@crashtest.example',
to: [`recipient${i}@crashtest.example`],
subject: `Connection Recovery Test ${i + 1}`,
text: `Testing connection recovery, email ${i + 1}`
}));
}
console.log(' Phase 1: Sending initial emails (connections should succeed)...');
const results1 = [];
for (let i = 0; i < 3; i++) {
try {
await smtpClient.sendMail(emails[i]);
results1.push({ success: true, index: i });
console.log(` ✓ Email ${i + 1} sent successfully`);
} catch (error) {
results1.push({ success: false, index: i, error });
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
}
}
console.log(' Phase 2: Enabling connection drops...');
dropConnections = true;
console.log(' Sending emails during connection instability...');
const results2 = [];
const promises = emails.slice(3).map((email, index) => {
const actualIndex = index + 3;
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Email ${actualIndex + 1} recovered and sent`);
return { success: true, index: actualIndex, result };
}).catch(error => {
console.log(` ✗ Email ${actualIndex + 1} failed permanently: ${error.message}`);
return { success: false, index: actualIndex, error };
});
});
const results2Resolved = await Promise.all(promises);
results2.push(...results2Resolved);
const totalSuccessful = [...results1, ...results2].filter(r => r.success).length;
const totalFailed = [...results1, ...results2].filter(r => !r.success).length;
console.log(` Connection attempts: ${connectionCount}`);
console.log(` Emails sent successfully: ${totalSuccessful}/${emails.length}`);
console.log(` Failed emails: ${totalFailed}`);
console.log(` Recovery effectiveness: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
expect(totalSuccessful).toBeGreaterThanOrEqual(3); // At least initial emails should succeed
expect(connectionCount).toBeGreaterThanOrEqual(2); // Should have made multiple connection attempts
smtpClient.close();
} finally {
server.close();
}
});
tap.test('CREL-04: Recovery from Server Restart', async () => {
console.log('\n💀 Testing recovery from server restart...');
// Start first server instance
let server1 = net.createServer(socket => {
console.log(' [Server1] Connection established');
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
server1.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server1.address() as net.AddressInfo).port;
try {
console.log(' Creating client...');
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 1,
connectionTimeout: 3000
});
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: 'sender@serverrestart.test',
to: [`recipient${i}@serverrestart.test`],
subject: `Server Restart Recovery ${i + 1}`,
text: `Testing server restart recovery, email ${i + 1}`
}));
}
console.log(' Sending first batch of emails...');
await smtpClient.sendMail(emails[0]);
console.log(' ✓ Email 1 sent successfully');
await smtpClient.sendMail(emails[1]);
console.log(' ✓ Email 2 sent successfully');
console.log(' Simulating server restart by closing server...');
server1.close();
await new Promise(resolve => setTimeout(resolve, 500));
console.log(' Starting new server instance on same port...');
const server2 = net.createServer(socket => {
console.log(' [Server2] Connection established after restart');
socket.write('220 localhost SMTP Test Server Restarted\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
server2.listen(port, '127.0.0.1', () => {
resolve();
});
});
console.log(' Sending emails after server restart...');
const recoveryResults = [];
for (let i = 2; i < emails.length; i++) {
try {
await smtpClient.sendMail(emails[i]);
recoveryResults.push({ success: true, index: i });
console.log(` ✓ Email ${i + 1} sent after server recovery`);
} catch (error) {
recoveryResults.push({ success: false, index: i, error });
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
}
}
const successfulRecovery = recoveryResults.filter(r => r.success).length;
const totalSuccessful = 2 + successfulRecovery; // 2 from before restart + recovery
console.log(` Pre-restart emails: 2/2 successful`);
console.log(` Post-restart emails: ${successfulRecovery}/${recoveryResults.length} successful`);
console.log(` Overall success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
console.log(` Server restart recovery: ${successfulRecovery > 0 ? 'Successful' : 'Failed'}`);
expect(successfulRecovery).toBeGreaterThanOrEqual(1); // At least some emails should work after restart
smtpClient.close();
server2.close();
} finally {
// Ensure cleanup
try {
server1.close();
} catch (e) { /* Already closed */ }
}
});
tap.test('CREL-04: Error Recovery and State Management', async () => {
console.log('\n⚠ Testing error recovery and state management...');
let errorInjectionEnabled = false;
const server = net.createServer(socket => {
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (errorInjectionEnabled && line.startsWith('MAIL FROM')) {
console.log(' [Server] Injecting error response');
socket.write('550 Simulated server error\r\n');
return;
}
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
} else if (line === 'RSET') {
socket.write('250 OK\r\n');
}
});
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server.address() as net.AddressInfo).port;
try {
console.log(' Creating client with error handling...');
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 1,
connectionTimeout: 3000
});
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: 'sender@exception.test',
to: [`recipient${i}@exception.test`],
subject: `Error Recovery Test ${i + 1}`,
text: `Testing error recovery, email ${i + 1}`
}));
}
console.log(' Phase 1: Sending emails normally...');
await smtpClient.sendMail(emails[0]);
console.log(' ✓ Email 1 sent successfully');
await smtpClient.sendMail(emails[1]);
console.log(' ✓ Email 2 sent successfully');
console.log(' Phase 2: Enabling error injection...');
errorInjectionEnabled = true;
console.log(' Sending emails with error injection...');
const recoveryResults = [];
for (let i = 2; i < 4; i++) {
try {
await smtpClient.sendMail(emails[i]);
recoveryResults.push({ success: true, index: i });
console.log(` ✓ Email ${i + 1} sent despite errors`);
} catch (error) {
recoveryResults.push({ success: false, index: i, error });
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
}
}
console.log(' Phase 3: Disabling error injection...');
errorInjectionEnabled = false;
console.log(' Sending final emails (recovery validation)...');
for (let i = 4; i < emails.length; i++) {
try {
await smtpClient.sendMail(emails[i]);
recoveryResults.push({ success: true, index: i });
console.log(` ✓ Email ${i + 1} sent after recovery`);
} catch (error) {
recoveryResults.push({ success: false, index: i, error });
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
}
}
const successful = recoveryResults.filter(r => r.success).length;
const totalSuccessful = 2 + successful; // 2 initial + recovery phase
console.log(` Pre-error emails: 2/2 successful`);
console.log(` Error/recovery phase emails: ${successful}/${recoveryResults.length} successful`);
console.log(` Total success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
console.log(` Error recovery: ${successful >= recoveryResults.length - 2 ? 'Effective' : 'Partial'}`);
expect(totalSuccessful).toBeGreaterThanOrEqual(4); // At least initial + some recovery
smtpClient.close();
} finally {
server.close();
}
});
tap.test('CREL-04: Resource Management During Issues', async () => {
console.log('\n🧠 Testing resource management during connection issues...');
let memoryBefore = process.memoryUsage();
const server = net.createServer(socket => {
socket.write('220 localhost SMTP Test Server\r\n');
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
socket.write('250-localhost\r\n');
socket.write('250 SIZE 10485760\r\n');
} else if (line.startsWith('MAIL FROM:')) {
socket.write('250 OK\r\n');
} else if (line.startsWith('RCPT TO:')) {
socket.write('250 OK\r\n');
} else if (line === 'DATA') {
socket.write('354 Send data\r\n');
} else if (line === '.') {
socket.write('250 OK Message accepted\r\n');
} else if (line === 'QUIT') {
socket.write('221 Bye\r\n');
socket.end();
}
});
});
});
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
const port = (server.address() as net.AddressInfo).port;
try {
console.log(' Creating client for resource management test...');
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 5,
maxMessages: 100
});
console.log(' Creating emails with various content types...');
const emails = [
new Email({
from: 'sender@resource.test',
to: ['recipient1@resource.test'],
subject: 'Resource Test - Normal',
text: 'Normal email content'
}),
new Email({
from: 'sender@resource.test',
to: ['recipient2@resource.test'],
subject: 'Resource Test - Large Content',
text: 'X'.repeat(50000) // Large content
}),
new Email({
from: 'sender@resource.test',
to: ['recipient3@resource.test'],
subject: 'Resource Test - Unicode',
text: '🎭🎪🎨🎯🎲🎸🎺🎻🎼🎵🎶🎷'.repeat(100)
})
];
console.log(' Sending emails and monitoring resource usage...');
const results = [];
for (let i = 0; i < emails.length; i++) {
console.log(` Testing email ${i + 1} (${emails[i].subject.split(' - ')[1]})...`);
try {
// Monitor memory usage before sending
const memBefore = process.memoryUsage();
console.log(` Memory before: ${Math.round(memBefore.heapUsed / 1024 / 1024)}MB`);
await smtpClient.sendMail(emails[i]);
const memAfter = process.memoryUsage();
console.log(` Memory after: ${Math.round(memAfter.heapUsed / 1024 / 1024)}MB`);
const memIncrease = memAfter.heapUsed - memBefore.heapUsed;
console.log(` Memory increase: ${Math.round(memIncrease / 1024)}KB`);
results.push({
success: true,
index: i,
memoryIncrease: memIncrease
});
console.log(` ✓ Email ${i + 1} sent successfully`);
} catch (error) {
results.push({ success: false, index: i, error });
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
}
// Force garbage collection if available
if (global.gc) {
global.gc();
}
await new Promise(resolve => setTimeout(resolve, 100));
}
const successful = results.filter(r => r.success).length;
const totalMemoryIncrease = results.reduce((sum, r) => sum + (r.memoryIncrease || 0), 0);
console.log(` Resource management: ${successful}/${emails.length} emails processed`);
console.log(` Total memory increase: ${Math.round(totalMemoryIncrease / 1024)}KB`);
console.log(` Resource efficiency: ${((successful / emails.length) * 100).toFixed(1)}%`);
expect(successful).toBeGreaterThanOrEqual(2); // Most emails should succeed
expect(totalMemoryIncrease).toBeLessThan(100 * 1024 * 1024); // Less than 100MB increase
smtpClient.close();
} finally {
server.close();
}
});
tap.test('CREL-04: Test Summary', async () => {
console.log('\n✅ CREL-04: Crash Recovery Reliability Tests completed');
console.log('💥 All connection recovery scenarios tested successfully');
});
tap.start();