520 lines
18 KiB
TypeScript
520 lines
18 KiB
TypeScript
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(); |