feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
This commit is contained in:
520
test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
Normal file
520
test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { createTestSmtpClient } from '../../helpers/smtp.client.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
|
||||
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();
|
||||
Reference in New Issue
Block a user