dcrouter/test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts

520 lines
18 KiB
TypeScript
Raw Permalink Normal View History

2025-05-26 14:50:55 +00:00
import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as net from 'net';
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
2025-05-24 18:12:08 +00:00
import { Email } from '../../../ts/mail/core/classes.email.js';
2025-05-26 14:50:55 +00:00
tap.test('CREL-04: Basic Connection Recovery from Server Issues', async () => {
console.log('\n💥 Testing SMTP Client Connection Recovery');
2025-05-24 18:12:08 +00:00
console.log('=' .repeat(60));
2025-05-26 14:50:55 +00:00
console.log('\n🔌 Testing recovery from connection drops...');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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`);
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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();
2025-05-24 18:12:08 +00:00
}
});
2025-05-26 14:50:55 +00:00
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const port = (server.address() as net.AddressInfo).port;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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}`
}));
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
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}`);
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
}
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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 };
2025-05-24 18:12:08 +00:00
});
2025-05-26 14:50:55 +00:00
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const results2Resolved = await Promise.all(promises);
results2.push(...results2Resolved);
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const totalSuccessful = [...results1, ...results2].filter(r => r.success).length;
const totalFailed = [...results1, ...results2].filter(r => !r.success).length;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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)}%`);
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
expect(totalSuccessful).toBeGreaterThanOrEqual(3); // At least initial emails should succeed
expect(connectionCount).toBeGreaterThanOrEqual(2); // Should have made multiple connection attempts
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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();
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
});
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
await new Promise<void>((resolve) => {
server1.listen(0, '127.0.0.1', () => {
resolve();
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const port = (server1.address() as net.AddressInfo).port;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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}`
}));
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
console.log(' Sending first batch of emails...');
await smtpClient.sendMail(emails[0]);
console.log(' ✓ Email 1 sent successfully');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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));
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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();
}
});
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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}`);
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
}
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const successfulRecovery = recoveryResults.filter(r => r.success).length;
const totalSuccessful = 2 + successfulRecovery; // 2 from before restart + recovery
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
socket.on('data', (data) => {
const lines = data.toString().split('\r\n');
lines.forEach(line => {
if (errorInjectionEnabled && line.startsWith('MAIL FROM')) {
2025-05-24 18:12:08 +00:00
console.log(' [Server] Injecting error response');
socket.write('550 Simulated server error\r\n');
2025-05-26 14:50:55 +00:00
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');
2025-05-24 18:12:08 +00:00
}
});
2025-05-26 14:50:55 +00:00
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const port = (server.address() as net.AddressInfo).port;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
try {
console.log(' Creating client with error handling...');
const smtpClient = createTestSmtpClient({
host: '127.0.0.1',
port: port,
secure: false,
maxConnections: 1,
connectionTimeout: 3000
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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}`
}));
}
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
console.log(' Phase 1: Sending emails normally...');
await smtpClient.sendMail(emails[0]);
console.log(' ✓ Email 1 sent successfully');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
await smtpClient.sendMail(emails[1]);
console.log(' ✓ Email 2 sent successfully');
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
console.log(' Phase 2: Enabling error injection...');
errorInjectionEnabled = true;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
console.log(' Sending emails with error injection...');
const recoveryResults = [];
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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}`);
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
}
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
console.log(' Phase 3: Disabling error injection...');
errorInjectionEnabled = false;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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}`);
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
}
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const successful = recoveryResults.filter(r => r.success).length;
const totalSuccessful = 2 + successful; // 2 initial + recovery phase
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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'}`);
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
expect(totalSuccessful).toBeGreaterThanOrEqual(4); // At least initial + some recovery
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
smtpClient.close();
} finally {
server.close();
}
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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();
}
2025-05-24 18:12:08 +00:00
});
2025-05-26 14:50:55 +00:00
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
await new Promise<void>((resolve) => {
server.listen(0, '127.0.0.1', () => {
resolve();
});
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
const port = (server.address() as net.AddressInfo).port;
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
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]})...`);
2025-05-24 18:12:08 +00:00
try {
2025-05-26 14:50:55 +00:00
// 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`);
2025-05-24 18:12:08 +00:00
} catch (error) {
2025-05-26 14:50:55 +00:00
results.push({ success: false, index: i, error });
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
// Force garbage collection if available
if (global.gc) {
global.gc();
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
await new Promise(resolve => setTimeout(resolve, 100));
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
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();
2025-05-24 18:12:08 +00:00
}
2025-05-26 14:50:55 +00:00
});
2025-05-24 18:12:08 +00:00
2025-05-26 14:50:55 +00:00
tap.test('CREL-04: Test Summary', async () => {
2025-05-24 18:12:08 +00:00
console.log('\n✅ CREL-04: Crash Recovery Reliability Tests completed');
2025-05-26 14:50:55 +00:00
console.log('💥 All connection recovery scenarios tested successfully');
});
tap.start();