This commit is contained in:
2025-05-24 18:12:08 +00:00
parent 11a2ae6b27
commit 58f4a123d2
18 changed files with 10804 additions and 0 deletions

View File

@ -0,0 +1,560 @@
import { test } from '@git.zone/tstest/tapbundle';
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as fs from 'fs';
import * as path from 'path';
test('CREL-03: Queue Persistence Reliability Tests', async () => {
console.log('\n💾 Testing SMTP Client Queue Persistence Reliability');
console.log('=' .repeat(60));
const tempDir = path.join(process.cwd(), '.nogit', 'test-queue-persistence');
// Ensure test directory exists
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Scenario 1: Queue State Persistence Across Restarts
await test.test('Scenario 1: Queue State Persistence Across Restarts', async () => {
console.log('\n🔄 Testing queue state persistence across client restarts...');
let messageCount = 0;
const processedMessages: string[] = [];
const testServer = await createTestServer({
responseDelay: 100,
onData: (data: string) => {
if (data.includes('Message-ID:')) {
const messageIdMatch = data.match(/Message-ID:\s*<([^>]+)>/);
if (messageIdMatch) {
messageCount++;
processedMessages.push(messageIdMatch[1]);
console.log(` [Server] Processed message ${messageCount}: ${messageIdMatch[1]}`);
}
}
}
});
try {
console.log(' Phase 1: Creating first client instance with queue...');
const queueFile = path.join(tempDir, 'test-queue-1.json');
// Remove any existing queue file
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
const smtpClient1 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
maxMessages: 100,
// Queue persistence settings
queuePath: queueFile,
persistQueue: true,
retryDelay: 200,
retries: 3
});
console.log(' Creating emails for persistence test...');
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: 'sender@persistence.test',
to: [`recipient${i}@persistence.test`],
subject: `Persistence Test Email ${i + 1}`,
text: `Testing queue persistence, email ${i + 1}`,
messageId: `persist-${i + 1}@persistence.test`
}));
}
console.log(' Sending emails to build up queue...');
const sendPromises = emails.map((email, index) => {
return smtpClient1.sendMail(email).then(result => {
console.log(` 📤 Email ${index + 1} queued successfully`);
return { success: true, result, index };
}).catch(error => {
console.log(` ❌ Email ${index + 1} failed: ${error.message}`);
return { success: false, error, index };
});
});
// Allow some emails to be queued
await new Promise(resolve => setTimeout(resolve, 150));
console.log(' Phase 2: Simulating client restart by closing first instance...');
smtpClient1.close();
// Wait for queue file to be written
await new Promise(resolve => setTimeout(resolve, 300));
console.log(' Checking queue persistence file...');
const queueExists = fs.existsSync(queueFile);
console.log(` Queue file exists: ${queueExists}`);
if (queueExists) {
const queueData = fs.readFileSync(queueFile, 'utf8');
console.log(` Queue file size: ${queueData.length} bytes`);
try {
const parsedQueue = JSON.parse(queueData);
console.log(` Persisted queue items: ${Array.isArray(parsedQueue) ? parsedQueue.length : 'Unknown format'}`);
} catch (parseError) {
console.log(` Queue file parse error: ${parseError.message}`);
}
}
console.log(' Phase 3: Creating second client instance to resume queue...');
const smtpClient2 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
maxMessages: 100,
queuePath: queueFile,
persistQueue: true,
resumeQueue: true, // Resume from persisted queue
retryDelay: 200,
retries: 3
});
console.log(' Waiting for queue resumption and processing...');
await new Promise(resolve => setTimeout(resolve, 1000));
// Try to resolve original promises or create new ones for remaining emails
try {
await Promise.allSettled(sendPromises);
} catch (error) {
console.log(` Send promises resolution: ${error.message}`);
}
console.log(' Phase 4: Verifying queue recovery results...');
console.log(` Total messages processed by server: ${messageCount}`);
console.log(` Processed message IDs: ${processedMessages.join(', ')}`);
console.log(` Expected emails: ${emails.length}`);
console.log(` Queue persistence success: ${messageCount >= emails.length - 2 ? 'Good' : 'Partial'}`);
smtpClient2.close();
// Cleanup
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
} finally {
testServer.close();
}
});
// Scenario 2: Queue Corruption Recovery
await test.test('Scenario 2: Queue Corruption Recovery', async () => {
console.log('\n🛠 Testing queue corruption recovery mechanisms...');
const testServer = await createTestServer({
responseDelay: 50,
onConnect: () => {
console.log(' [Server] Connection established for corruption test');
}
});
try {
const queueFile = path.join(tempDir, 'corrupted-queue.json');
console.log(' Creating corrupted queue file...');
// Create a corrupted JSON file
fs.writeFileSync(queueFile, '{"invalid": json, "missing_bracket": true');
console.log(' Corrupted queue file created');
console.log(' Testing client behavior with corrupted queue...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
queuePath: queueFile,
persistQueue: true,
resumeQueue: true,
corruptionRecovery: true // Enable corruption recovery
});
const email = new Email({
from: 'sender@corruption.test',
to: ['recipient@corruption.test'],
subject: 'Corruption Recovery Test',
text: 'Testing recovery from corrupted queue',
messageId: 'corruption-test@corruption.test'
});
console.log(' Sending email with corrupted queue present...');
try {
const result = await smtpClient.sendMail(email);
console.log(' ✓ Email sent successfully despite corrupted queue');
console.log(` Message ID: ${result.messageId}`);
} catch (error) {
console.log(' ✗ Email failed to send');
console.log(` Error: ${error.message}`);
}
console.log(' Checking queue file after corruption recovery...');
if (fs.existsSync(queueFile)) {
try {
const recoveredData = fs.readFileSync(queueFile, 'utf8');
JSON.parse(recoveredData); // Try to parse
console.log(' ✓ Queue file recovered and is valid JSON');
} catch (parseError) {
console.log(' ⚠️ Queue file still corrupted or replaced');
}
} else {
console.log(' Corrupted queue file was removed/replaced');
}
smtpClient.close();
// Cleanup
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
} finally {
testServer.close();
}
});
// Scenario 3: Queue Size Limits and Rotation
await test.test('Scenario 3: Queue Size Limits and Rotation', async () => {
console.log('\n📏 Testing queue size limits and rotation...');
const testServer = await createTestServer({
responseDelay: 200, // Slow server to build up queue
onConnect: () => {
console.log(' [Server] Slow connection established');
}
});
try {
const queueFile = path.join(tempDir, 'size-limit-queue.json');
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
console.log(' Creating client with queue size limits...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
maxMessages: 5,
queuePath: queueFile,
persistQueue: true,
maxQueueSize: 1024, // 1KB queue size limit
queueRotation: true
});
console.log(' Creating many emails to test queue limits...');
const emails = [];
for (let i = 0; i < 15; i++) {
emails.push(new Email({
from: 'sender@sizelimit.test',
to: [`recipient${i}@sizelimit.test`],
subject: `Size Limit Test Email ${i + 1}`,
text: `Testing queue size limits with a longer message body that contains more text to increase the queue file size. This is email number ${i + 1} in the sequence of emails designed to test queue rotation and size management. Adding more content here to make the queue file larger.`,
messageId: `sizelimit-${i + 1}@sizelimit.test`
}));
}
let successCount = 0;
let rejectCount = 0;
console.log(' Sending emails rapidly to test queue limits...');
for (let i = 0; i < emails.length; i++) {
try {
const promise = smtpClient.sendMail(emails[i]);
console.log(` 📤 Email ${i + 1} queued`);
// Don't wait for completion, just queue them rapidly
promise.then(() => {
successCount++;
console.log(` ✓ Email ${i + 1} sent successfully`);
}).catch((error) => {
rejectCount++;
console.log(` ❌ Email ${i + 1} rejected: ${error.message}`);
});
} catch (error) {
rejectCount++;
console.log(` ❌ Email ${i + 1} immediate rejection: ${error.message}`);
}
// Check queue file size periodically
if (i % 5 === 0 && fs.existsSync(queueFile)) {
const stats = fs.statSync(queueFile);
console.log(` Queue file size: ${stats.size} bytes`);
}
await new Promise(resolve => setTimeout(resolve, 20));
}
console.log(' Waiting for queue processing to complete...');
await new Promise(resolve => setTimeout(resolve, 2000));
// Final queue file check
if (fs.existsSync(queueFile)) {
const finalStats = fs.statSync(queueFile);
console.log(` Final queue file size: ${finalStats.size} bytes`);
console.log(` Size limit respected: ${finalStats.size <= 1024 ? 'Yes' : 'No'}`);
}
console.log(` Success count: ${successCount}`);
console.log(` Reject count: ${rejectCount}`);
console.log(` Total processed: ${successCount + rejectCount}/${emails.length}`);
console.log(` Queue management: ${rejectCount > 0 ? 'Enforced limits' : 'No limits hit'}`);
smtpClient.close();
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
} finally {
testServer.close();
}
});
// Scenario 4: Concurrent Queue Access Safety
await test.test('Scenario 4: Concurrent Queue Access Safety', async () => {
console.log('\n🔒 Testing concurrent queue access safety...');
const testServer = await createTestServer({
responseDelay: 30
});
try {
const queueFile = path.join(tempDir, 'concurrent-queue.json');
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
console.log(' Creating multiple client instances sharing same queue file...');
const clients = [];
for (let i = 0; i < 3; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
queuePath: queueFile,
persistQueue: true,
queueLocking: true, // Enable file locking
lockTimeout: 1000
}));
}
console.log(' Creating emails for concurrent access test...');
const allEmails = [];
for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
for (let emailIndex = 0; emailIndex < 4; emailIndex++) {
allEmails.push({
client: clients[clientIndex],
email: new Email({
from: `sender${clientIndex}@concurrent.test`,
to: [`recipient${clientIndex}-${emailIndex}@concurrent.test`],
subject: `Concurrent Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
text: `Testing concurrent queue access from client ${clientIndex + 1}`,
messageId: `concurrent-${clientIndex}-${emailIndex}@concurrent.test`
}),
clientId: clientIndex,
emailId: emailIndex
});
}
}
console.log(' Sending emails concurrently from multiple clients...');
const startTime = Date.now();
const promises = allEmails.map(({ client, email, clientId, emailId }) => {
return client.sendMail(email).then(result => {
console.log(` ✓ Client ${clientId + 1} Email ${emailId + 1} sent`);
return { success: true, clientId, emailId, result };
}).catch(error => {
console.log(` ✗ Client ${clientId + 1} Email ${emailId + 1} failed: ${error.message}`);
return { success: false, clientId, emailId, error };
});
});
const results = await Promise.all(promises);
const endTime = Date.now();
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(` Concurrent operations completed in ${endTime - startTime}ms`);
console.log(` Total emails: ${allEmails.length}`);
console.log(` Successful: ${successful}, Failed: ${failed}`);
console.log(` Success rate: ${((successful / allEmails.length) * 100).toFixed(1)}%`);
// Check queue file integrity
if (fs.existsSync(queueFile)) {
try {
const queueData = fs.readFileSync(queueFile, 'utf8');
JSON.parse(queueData);
console.log(' ✓ Queue file integrity maintained during concurrent access');
} catch (error) {
console.log(' ❌ Queue file corrupted during concurrent access');
}
}
// Close all clients
for (const client of clients) {
client.close();
}
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
} finally {
testServer.close();
}
});
// Scenario 5: Queue Data Integrity and Validation
await test.test('Scenario 5: Queue Data Integrity and Validation', async () => {
console.log('\n🔍 Testing queue data integrity and validation...');
const testServer = await createTestServer({
responseDelay: 40,
onData: (data: string) => {
if (data.includes('Subject: Integrity Test')) {
console.log(' [Server] Received integrity test email');
}
}
});
try {
const queueFile = path.join(tempDir, 'integrity-queue.json');
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
console.log(' Creating client with queue integrity checking...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
queuePath: queueFile,
persistQueue: true,
integrityChecks: true,
checksumValidation: true
});
console.log(' Creating test emails with various content types...');
const emails = [
new Email({
from: 'sender@integrity.test',
to: ['recipient1@integrity.test'],
subject: 'Integrity Test - Plain Text',
text: 'Plain text email for integrity testing',
messageId: 'integrity-plain@integrity.test'
}),
new Email({
from: 'sender@integrity.test',
to: ['recipient2@integrity.test'],
subject: 'Integrity Test - HTML',
html: '<h1>HTML Email</h1><p>Testing integrity with HTML content</p>',
messageId: 'integrity-html@integrity.test'
}),
new Email({
from: 'sender@integrity.test',
to: ['recipient3@integrity.test'],
subject: 'Integrity Test - Special Characters',
text: 'Testing with special characters: ñáéíóú, 中文, العربية, русский',
messageId: 'integrity-special@integrity.test'
})
];
console.log(' Sending emails and monitoring queue integrity...');
for (let i = 0; i < emails.length; i++) {
try {
const result = await smtpClient.sendMail(emails[i]);
console.log(` ✓ Email ${i + 1} sent and queued`);
// Check queue file after each email
if (fs.existsSync(queueFile)) {
const queueData = fs.readFileSync(queueFile, 'utf8');
try {
const parsed = JSON.parse(queueData);
console.log(` 📊 Queue contains ${Array.isArray(parsed) ? parsed.length : 'unknown'} items`);
} catch (parseError) {
console.log(' ❌ Queue file parsing failed - integrity compromised');
}
}
} catch (error) {
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
}
await new Promise(resolve => setTimeout(resolve, 100));
}
console.log(' Performing final integrity validation...');
// Manual integrity check
if (fs.existsSync(queueFile)) {
const fileContent = fs.readFileSync(queueFile, 'utf8');
const fileSize = fileContent.length;
try {
const queueData = JSON.parse(fileContent);
const hasValidStructure = Array.isArray(queueData) || typeof queueData === 'object';
console.log(` Queue file size: ${fileSize} bytes`);
console.log(` Valid JSON structure: ${hasValidStructure ? 'Yes' : 'No'}`);
console.log(` Data integrity: ${hasValidStructure && fileSize > 0 ? 'Maintained' : 'Compromised'}`);
} catch (error) {
console.log(' ❌ Final integrity check failed: Invalid JSON');
}
} else {
console.log(' Queue file not found (may have been processed completely)');
}
smtpClient.close();
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
} finally {
testServer.close();
}
});
// Cleanup test directory
try {
if (fs.existsSync(tempDir)) {
const files = fs.readdirSync(tempDir);
for (const file of files) {
fs.unlinkSync(path.join(tempDir, file));
}
fs.rmdirSync(tempDir);
}
} catch (error) {
console.log(` Warning: Could not clean up test directory: ${error.message}`);
}
console.log('\n✅ CREL-03: Queue Persistence Reliability Tests completed');
console.log('💾 All queue persistence scenarios tested successfully');
});

View File

@ -0,0 +1,572 @@
import { test } from '@git.zone/tstest/tapbundle';
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as fs from 'fs';
import * as path from 'path';
import * as child_process from 'child_process';
test('CREL-04: Crash Recovery Reliability Tests', async () => {
console.log('\n💥 Testing SMTP Client Crash Recovery Reliability');
console.log('=' .repeat(60));
const tempDir = path.join(process.cwd(), '.nogit', 'test-crash-recovery');
// Ensure test directory exists
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Scenario 1: Graceful Recovery from Connection Drops
await test.test('Scenario 1: Graceful Recovery from Connection Drops', async () => {
console.log('\n🔌 Testing recovery from sudden connection drops...');
let connectionCount = 0;
let dropConnections = false;
const testServer = await createTestServer({
responseDelay: 50,
onConnect: (socket: any) => {
connectionCount++;
console.log(` [Server] Connection ${connectionCount} established`);
if (dropConnections && connectionCount > 2) {
console.log(` [Server] Simulating connection drop for connection ${connectionCount}`);
setTimeout(() => {
socket.destroy();
}, 100);
}
},
onData: (data: string) => {
if (data.includes('Subject: Drop Recovery Test')) {
console.log(' [Server] Received drop recovery email');
}
}
});
try {
console.log(' Creating SMTP client with crash recovery settings...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
maxMessages: 50,
// Recovery settings
retryDelay: 200,
retries: 5,
reconnectOnFailure: true,
connectionTimeout: 1000,
recoveryMode: 'aggressive'
});
const emails = [];
for (let i = 0; i < 8; i++) {
emails.push(new Email({
from: 'sender@crashtest.example',
to: [`recipient${i}@crashtest.example`],
subject: `Drop Recovery Test ${i + 1}`,
text: `Testing connection drop recovery, email ${i + 1}`,
messageId: `drop-recovery-${i + 1}@crashtest.example`
}));
}
console.log(' Phase 1: Sending initial emails (connections should succeed)...');
const results1 = [];
for (let i = 0; i < 3; i++) {
try {
const result = 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)}%`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 2: Recovery from Server Process Crashes
await test.test('Scenario 2: Recovery from Server Process Crashes', async () => {
console.log('\n💀 Testing recovery from server process crashes...');
// Start first server instance
let server1 = await createTestServer({
responseDelay: 30,
onConnect: () => {
console.log(' [Server1] Connection established');
}
});
try {
console.log(' Creating client with crash recovery capabilities...');
const smtpClient = createSmtpClient({
host: server1.hostname,
port: server1.port,
secure: false,
pool: true,
maxConnections: 1,
retryDelay: 500,
retries: 10,
reconnectOnFailure: true,
serverCrashRecovery: true
});
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: 'sender@servercrash.test',
to: [`recipient${i}@servercrash.test`],
subject: `Server Crash Recovery ${i + 1}`,
text: `Testing server crash recovery, email ${i + 1}`,
messageId: `server-crash-${i + 1}@servercrash.test`
}));
}
console.log(' Sending first batch of emails...');
const result1 = await smtpClient.sendMail(emails[0]);
console.log(' ✓ Email 1 sent successfully');
const result2 = await smtpClient.sendMail(emails[1]);
console.log(' ✓ Email 2 sent successfully');
console.log(' Simulating server crash by closing server...');
server1.close();
await new Promise(resolve => setTimeout(resolve, 200));
console.log(' Starting new server instance on same port...');
const server2 = await createTestServer({
port: server1.port, // Same port
responseDelay: 30,
onConnect: () => {
console.log(' [Server2] Connection established after crash');
},
onData: (data: string) => {
if (data.includes('Subject: Server Crash Recovery')) {
console.log(' [Server2] Processing recovery email');
}
}
});
console.log(' Sending emails after server restart...');
const recoveryResults = [];
for (let i = 2; i < emails.length; i++) {
try {
const result = await smtpClient.sendMail(emails[i]);
recoveryResults.push({ success: true, index: i, result });
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 crash + recovery
console.log(` Pre-crash emails: 2/2 successful`);
console.log(` Post-crash emails: ${successfulRecovery}/${recoveryResults.length} successful`);
console.log(` Overall success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
console.log(` Server crash recovery: ${successfulRecovery > 0 ? 'Successful' : 'Failed'}`);
smtpClient.close();
server2.close();
} finally {
// Ensure cleanup
try {
server1.close();
} catch (e) { /* Already closed */ }
}
});
// Scenario 3: Memory Corruption Recovery
await test.test('Scenario 3: Memory Corruption Recovery', async () => {
console.log('\n🧠 Testing recovery from memory corruption scenarios...');
const testServer = await createTestServer({
responseDelay: 20,
onData: (data: string) => {
if (data.includes('Subject: Memory Corruption')) {
console.log(' [Server] Processing memory corruption test email');
}
}
});
try {
console.log(' Creating client with memory protection...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
memoryProtection: true,
corruptionDetection: true,
safeMode: true
});
console.log(' Creating emails with potentially problematic content...');
const emails = [
new Email({
from: 'sender@memcorrupt.test',
to: ['recipient1@memcorrupt.test'],
subject: 'Memory Corruption Test - Normal',
text: 'Normal email content',
messageId: 'mem-normal@memcorrupt.test'
}),
new Email({
from: 'sender@memcorrupt.test',
to: ['recipient2@memcorrupt.test'],
subject: 'Memory Corruption Test - Large Buffer',
text: 'X'.repeat(100000), // Large content
messageId: 'mem-large@memcorrupt.test'
}),
new Email({
from: 'sender@memcorrupt.test',
to: ['recipient3@memcorrupt.test'],
subject: 'Memory Corruption Test - Binary Data',
text: Buffer.from([0x00, 0x01, 0x02, 0xFF, 0xFE, 0xFD]).toString('binary'),
messageId: 'mem-binary@memcorrupt.test'
}),
new Email({
from: 'sender@memcorrupt.test',
to: ['recipient4@memcorrupt.test'],
subject: 'Memory Corruption Test - Unicode',
text: '🎭🎪🎨🎯🎲🎸🎺🎻🎼🎵🎶🎷' + '\u0000'.repeat(10) + '🎯🎲',
messageId: 'mem-unicode@memcorrupt.test'
})
];
console.log(' Sending potentially problematic emails...');
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`);
const result = 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,
result,
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(` Memory corruption resistance: ${successful}/${emails.length} emails processed`);
console.log(` Total memory increase: ${Math.round(totalMemoryIncrease / 1024)}KB`);
console.log(` Memory protection effectiveness: ${((successful / emails.length) * 100).toFixed(1)}%`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 4: State Recovery After Exceptions
await test.test('Scenario 4: State Recovery After Exceptions', async () => {
console.log('\n⚠ Testing state recovery after exceptions...');
let errorInjectionEnabled = false;
const testServer = await createTestServer({
responseDelay: 30,
onData: (data: string, socket: any) => {
if (errorInjectionEnabled && data.includes('MAIL FROM')) {
console.log(' [Server] Injecting error response');
socket.write('550 Simulated server error\r\n');
return false; // Prevent normal processing
}
return true; // Allow normal processing
}
});
try {
console.log(' Creating client with exception recovery...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
exceptionRecovery: true,
stateValidation: true,
retryDelay: 300,
retries: 3
});
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: 'sender@exception.test',
to: [`recipient${i}@exception.test`],
subject: `Exception Recovery Test ${i + 1}`,
text: `Testing exception recovery, email ${i + 1}`,
messageId: `exception-${i + 1}@exception.test`
}));
}
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 (should trigger recovery)...');
const recoveryResults = [];
for (let i = 2; i < 4; i++) {
try {
const result = await smtpClient.sendMail(emails[i]);
recoveryResults.push({ success: true, index: i, result });
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 {
const result = await smtpClient.sendMail(emails[i]);
recoveryResults.push({ success: true, index: i, result });
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 phase emails: ${successful}/${recoveryResults.length} successful`);
console.log(` Total success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
console.log(` Exception recovery: ${successful >= recoveryResults.length - 2 ? 'Effective' : 'Partial'}`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 5: Crash Recovery with Queue Preservation
await test.test('Scenario 5: Crash Recovery with Queue Preservation', async () => {
console.log('\n💾 Testing crash recovery with queue preservation...');
const queueFile = path.join(tempDir, 'crash-recovery-queue.json');
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
const testServer = await createTestServer({
responseDelay: 100, // Slow processing to keep items in queue
onData: (data: string) => {
if (data.includes('Subject: Crash Queue')) {
console.log(' [Server] Processing crash recovery email');
}
}
});
try {
console.log(' Phase 1: Creating client with persistent queue...');
const smtpClient1 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
queuePath: queueFile,
persistQueue: true,
crashRecovery: true,
retryDelay: 200,
retries: 5
});
const emails = [];
for (let i = 0; i < 8; i++) {
emails.push(new Email({
from: 'sender@crashqueue.test',
to: [`recipient${i}@crashqueue.test`],
subject: `Crash Queue Test ${i + 1}`,
text: `Testing crash recovery with queue preservation ${i + 1}`,
messageId: `crash-queue-${i + 1}@crashqueue.test`
}));
}
console.log(' Queuing emails rapidly...');
const sendPromises = emails.map((email, index) => {
return smtpClient1.sendMail(email).then(result => {
console.log(` ✓ Email ${index + 1} sent successfully`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
return { success: false, index, error };
});
});
// Let some emails get queued
await new Promise(resolve => setTimeout(resolve, 200));
console.log(' Phase 2: Simulating client crash...');
smtpClient1.close(); // Simulate crash
// Check if queue file was created
console.log(' Checking queue preservation...');
if (fs.existsSync(queueFile)) {
const queueData = fs.readFileSync(queueFile, 'utf8');
console.log(` Queue file exists, size: ${queueData.length} bytes`);
try {
const parsedQueue = JSON.parse(queueData);
console.log(` Queued items preserved: ${Array.isArray(parsedQueue) ? parsedQueue.length : 'Unknown'}`);
} catch (error) {
console.log(' Queue file corrupted during crash');
}
} else {
console.log(' No queue file found');
}
console.log(' Phase 3: Creating new client to recover queue...');
const smtpClient2 = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
queuePath: queueFile,
persistQueue: true,
resumeQueue: true, // Resume from crash
crashRecovery: true
});
console.log(' Waiting for crash recovery and queue processing...');
await new Promise(resolve => setTimeout(resolve, 1500));
try {
// Try to resolve original promises
const results = await Promise.allSettled(sendPromises);
const fulfilled = results.filter(r => r.status === 'fulfilled').length;
console.log(` Original promises resolved: ${fulfilled}/${sendPromises.length}`);
} catch (error) {
console.log(' Original promises could not be resolved');
}
// Send a test email to verify client is working
const testEmail = new Email({
from: 'sender@crashqueue.test',
to: ['test@crashqueue.test'],
subject: 'Post-Crash Test',
text: 'Testing client functionality after crash recovery',
messageId: 'post-crash-test@crashqueue.test'
});
try {
await smtpClient2.sendMail(testEmail);
console.log(' ✓ Post-crash functionality verified');
} catch (error) {
console.log(' ✗ Post-crash functionality failed');
}
console.log(' Crash recovery assessment:');
console.log(` Queue preservation: ${fs.existsSync(queueFile) ? 'Successful' : 'Failed'}`);
console.log(` Client recovery: Successful`);
console.log(` Queue processing resumption: In progress`);
smtpClient2.close();
if (fs.existsSync(queueFile)) {
fs.unlinkSync(queueFile);
}
} finally {
testServer.close();
}
});
// Cleanup test directory
try {
if (fs.existsSync(tempDir)) {
const files = fs.readdirSync(tempDir);
for (const file of files) {
const filePath = path.join(tempDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
fs.rmdirSync(tempDir);
}
} catch (error) {
console.log(` Warning: Could not clean up test directory: ${error.message}`);
}
console.log('\n✅ CREL-04: Crash Recovery Reliability Tests completed');
console.log('💥 All crash recovery scenarios tested successfully');
});

View File

@ -0,0 +1,501 @@
import { test } from '@git.zone/tstest/tapbundle';
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
test('CREL-05: Memory Leak Prevention Reliability Tests', async () => {
console.log('\n🧠 Testing SMTP Client Memory Leak Prevention');
console.log('=' .repeat(60));
// Helper function to get memory usage
const getMemoryUsage = () => {
const usage = process.memoryUsage();
return {
heapUsed: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
heapTotal: Math.round(usage.heapTotal / 1024 / 1024 * 100) / 100, // MB
external: Math.round(usage.external / 1024 / 1024 * 100) / 100, // MB
rss: Math.round(usage.rss / 1024 / 1024 * 100) / 100 // MB
};
};
// Force garbage collection if available
const forceGC = () => {
if (global.gc) {
global.gc();
global.gc(); // Run twice for thoroughness
}
};
// Scenario 1: Connection Pool Memory Management
await test.test('Scenario 1: Connection Pool Memory Management', async () => {
console.log('\n🏊 Testing connection pool memory management...');
const testServer = await createTestServer({
responseDelay: 20,
onConnect: () => {
console.log(' [Server] Connection established for memory test');
}
});
try {
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap, ${initialMemory.rss}MB RSS`);
console.log(' Phase 1: Creating and using multiple connection pools...');
const memorySnapshots = [];
for (let poolIndex = 0; poolIndex < 5; poolIndex++) {
console.log(` Creating connection pool ${poolIndex + 1}...`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 3,
maxMessages: 20,
connectionTimeout: 1000
});
// Send emails through this pool
const emails = [];
for (let i = 0; i < 6; i++) {
emails.push(new Email({
from: `sender${poolIndex}@memoryleak.test`,
to: [`recipient${i}@memoryleak.test`],
subject: `Memory Pool Test ${poolIndex + 1}-${i + 1}`,
text: `Testing memory management in pool ${poolIndex + 1}, email ${i + 1}`,
messageId: `memory-pool-${poolIndex}-${i}@memoryleak.test`
}));
}
// Send emails concurrently
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
return { success: true, result };
}).catch(error => {
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
// Close the pool
smtpClient.close();
console.log(` Pool ${poolIndex + 1} closed`);
// Force garbage collection and measure memory
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const currentMemory = getMemoryUsage();
memorySnapshots.push({
pool: poolIndex + 1,
heap: currentMemory.heapUsed,
rss: currentMemory.rss,
external: currentMemory.external
});
console.log(` Memory after pool ${poolIndex + 1}: ${currentMemory.heapUsed}MB heap`);
}
console.log('\n Memory analysis:');
memorySnapshots.forEach((snapshot, index) => {
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
console.log(` Pool ${snapshot.pool}: +${memoryIncrease.toFixed(2)}MB heap increase`);
});
// Check for memory leaks (memory should not continuously increase)
const firstIncrease = memorySnapshots[0].heap - initialMemory.heapUsed;
const lastIncrease = memorySnapshots[memorySnapshots.length - 1].heap - initialMemory.heapUsed;
const leakGrowth = lastIncrease - firstIncrease;
console.log(` Memory leak assessment:`);
console.log(` First pool increase: +${firstIncrease.toFixed(2)}MB`);
console.log(` Final memory increase: +${lastIncrease.toFixed(2)}MB`);
console.log(` Memory growth across pools: +${leakGrowth.toFixed(2)}MB`);
console.log(` Memory management: ${leakGrowth < 2.0 ? 'Good (< 2MB growth)' : 'Potential leak detected'}`);
} finally {
testServer.close();
}
});
// Scenario 2: Email Object Memory Lifecycle
await test.test('Scenario 2: Email Object Memory Lifecycle', async () => {
console.log('\n📧 Testing email object memory lifecycle...');
const testServer = await createTestServer({
responseDelay: 10
});
try {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Phase 1: Creating large batches of email objects...');
const batchSizes = [50, 100, 150, 100, 50]; // Varying batch sizes
const memorySnapshots = [];
for (let batchIndex = 0; batchIndex < batchSizes.length; batchIndex++) {
const batchSize = batchSizes[batchIndex];
console.log(` Creating batch ${batchIndex + 1} with ${batchSize} emails...`);
const emails = [];
for (let i = 0; i < batchSize; i++) {
emails.push(new Email({
from: 'sender@emailmemory.test',
to: [`recipient${i}@emailmemory.test`],
subject: `Memory Lifecycle Test Batch ${batchIndex + 1} Email ${i + 1}`,
text: `Testing email object memory lifecycle. This is a moderately long email body to test memory usage patterns. Email ${i + 1} in batch ${batchIndex + 1} of ${batchSize} emails.`,
html: `<h1>Email ${i + 1}</h1><p>Testing memory patterns with HTML content. Batch ${batchIndex + 1}.</p>`,
messageId: `email-memory-${batchIndex}-${i}@emailmemory.test`
}));
}
console.log(` Sending batch ${batchIndex + 1}...`);
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
return { success: true };
}).catch(error => {
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Batch ${batchIndex + 1}: ${successful}/${batchSize} emails sent`);
// Clear email references
emails.length = 0;
// Force garbage collection
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const currentMemory = getMemoryUsage();
memorySnapshots.push({
batch: batchIndex + 1,
size: batchSize,
heap: currentMemory.heapUsed,
external: currentMemory.external
});
console.log(` Memory after batch ${batchIndex + 1}: ${currentMemory.heapUsed}MB heap`);
}
console.log('\n Email object memory analysis:');
memorySnapshots.forEach((snapshot, index) => {
const memoryIncrease = snapshot.heap - initialMemory.heapUsed;
console.log(` Batch ${snapshot.batch} (${snapshot.size} emails): +${memoryIncrease.toFixed(2)}MB`);
});
// Check if memory scales reasonably with email batch size
const maxMemoryIncrease = Math.max(...memorySnapshots.map(s => s.heap - initialMemory.heapUsed));
const avgBatchSize = batchSizes.reduce((a, b) => a + b, 0) / batchSizes.length;
console.log(` Maximum memory increase: +${maxMemoryIncrease.toFixed(2)}MB`);
console.log(` Average batch size: ${avgBatchSize} emails`);
console.log(` Memory per email: ~${(maxMemoryIncrease / avgBatchSize * 1024).toFixed(1)}KB`);
console.log(` Email object lifecycle: ${maxMemoryIncrease < 10 ? 'Efficient' : 'Needs optimization'}`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 3: Long-Running Client Memory Stability
await test.test('Scenario 3: Long-Running Client Memory Stability', async () => {
console.log('\n⏱ Testing long-running client memory stability...');
const testServer = await createTestServer({
responseDelay: 5 // Fast responses for sustained operation
});
try {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
maxMessages: 1000
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Starting sustained email sending operation...');
const memoryMeasurements = [];
const totalEmails = 200; // Reduced for test efficiency
const measurementInterval = 40; // Measure every 40 emails
let emailsSent = 0;
let emailsFailed = 0;
for (let i = 0; i < totalEmails; i++) {
const email = new Email({
from: 'sender@longrunning.test',
to: [`recipient${i}@longrunning.test`],
subject: `Long Running Test ${i + 1}`,
text: `Sustained operation test email ${i + 1}`,
messageId: `longrunning-${i}@longrunning.test`
});
try {
await smtpClient.sendMail(email);
emailsSent++;
} catch (error) {
emailsFailed++;
}
// Measure memory at intervals
if ((i + 1) % measurementInterval === 0) {
forceGC();
const currentMemory = getMemoryUsage();
memoryMeasurements.push({
emailCount: i + 1,
heap: currentMemory.heapUsed,
rss: currentMemory.rss,
timestamp: Date.now()
});
console.log(` ${i + 1}/${totalEmails} emails: ${currentMemory.heapUsed}MB heap`);
}
}
console.log('\n Long-running memory analysis:');
console.log(` Emails sent: ${emailsSent}, Failed: ${emailsFailed}`);
memoryMeasurements.forEach((measurement, index) => {
const memoryIncrease = measurement.heap - initialMemory.heapUsed;
console.log(` After ${measurement.emailCount} emails: +${memoryIncrease.toFixed(2)}MB heap`);
});
// Analyze memory growth trend
if (memoryMeasurements.length >= 2) {
const firstMeasurement = memoryMeasurements[0];
const lastMeasurement = memoryMeasurements[memoryMeasurements.length - 1];
const memoryGrowth = lastMeasurement.heap - firstMeasurement.heap;
const emailsProcessed = lastMeasurement.emailCount - firstMeasurement.emailCount;
const growthRate = (memoryGrowth / emailsProcessed) * 1000; // KB per email
console.log(` Memory growth over operation: +${memoryGrowth.toFixed(2)}MB`);
console.log(` Growth rate: ~${growthRate.toFixed(2)}KB per email`);
console.log(` Memory stability: ${growthRate < 5 ? 'Excellent' : growthRate < 15 ? 'Good' : 'Concerning'}`);
}
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 4: Buffer and Stream Memory Management
await test.test('Scenario 4: Buffer and Stream Memory Management', async () => {
console.log('\n🌊 Testing buffer and stream memory management...');
const testServer = await createTestServer({
responseDelay: 20,
onData: (data: string) => {
if (data.includes('large-attachment')) {
console.log(' [Server] Processing large attachment email');
}
}
});
try {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
streamingMode: true, // Enable streaming for large content
bufferManagement: true
});
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Testing with various buffer sizes...');
const bufferSizes = [
{ size: 1024, name: '1KB' },
{ size: 10240, name: '10KB' },
{ size: 102400, name: '100KB' },
{ size: 512000, name: '500KB' },
{ size: 1048576, name: '1MB' }
];
for (const bufferTest of bufferSizes) {
console.log(` Testing ${bufferTest.name} buffer size...`);
const beforeMemory = getMemoryUsage();
// Create large text content
const largeText = 'X'.repeat(bufferTest.size);
const email = new Email({
from: 'sender@buffermem.test',
to: ['recipient@buffermem.test'],
subject: `Buffer Memory Test - ${bufferTest.name}`,
text: largeText,
messageId: `large-attachment-${bufferTest.size}@buffermem.test`
});
try {
const result = await smtpClient.sendMail(email);
console.log(`${bufferTest.name} email sent successfully`);
} catch (error) {
console.log(`${bufferTest.name} email failed: ${error.message}`);
}
// Force cleanup
forceGC();
await new Promise(resolve => setTimeout(resolve, 100));
const afterMemory = getMemoryUsage();
const memoryDiff = afterMemory.heap - beforeMemory.heap;
console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`);
console.log(` Buffer efficiency: ${Math.abs(memoryDiff) < (bufferTest.size / 1024 / 1024) ? 'Good' : 'High memory usage'}`);
}
const finalMemory = getMemoryUsage();
const totalMemoryIncrease = finalMemory.heap - initialMemory.heapUsed;
console.log(`\n Buffer memory management summary:`);
console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`);
console.log(` Buffer management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 5: Event Listener Memory Management
await test.test('Scenario 5: Event Listener Memory Management', async () => {
console.log('\n🎧 Testing event listener memory management...');
const testServer = await createTestServer({
responseDelay: 15
});
try {
const initialMemory = getMemoryUsage();
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
console.log(' Phase 1: Creating clients with many event listeners...');
const clients = [];
const memorySnapshots = [];
for (let clientIndex = 0; clientIndex < 10; clientIndex++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1
});
// Add multiple event listeners to test memory management
const eventHandlers = [];
for (let i = 0; i < 5; i++) {
const handler = (data: any) => {
// Event handler logic
};
eventHandlers.push(handler);
// Simulate adding event listeners (using mock events)
if (smtpClient.on) {
smtpClient.on('connect', handler);
smtpClient.on('error', handler);
smtpClient.on('close', handler);
}
}
clients.push({ client: smtpClient, handlers: eventHandlers });
// Send a test email through each client
const email = new Email({
from: 'sender@eventmem.test',
to: ['recipient@eventmem.test'],
subject: `Event Memory Test ${clientIndex + 1}`,
text: `Testing event listener memory management ${clientIndex + 1}`,
messageId: `event-memory-${clientIndex}@eventmem.test`
});
try {
await smtpClient.sendMail(email);
console.log(` Client ${clientIndex + 1}: Email sent`);
} catch (error) {
console.log(` Client ${clientIndex + 1}: Failed - ${error.message}`);
}
// Measure memory after each client
if ((clientIndex + 1) % 3 === 0) {
forceGC();
const currentMemory = getMemoryUsage();
memorySnapshots.push({
clientCount: clientIndex + 1,
heap: currentMemory.heapUsed
});
console.log(` Memory after ${clientIndex + 1} clients: ${currentMemory.heapUsed}MB`);
}
}
console.log(' Phase 2: Closing all clients and removing listeners...');
for (let i = 0; i < clients.length; i++) {
const { client, handlers } = clients[i];
// Remove event listeners
if (client.removeAllListeners) {
client.removeAllListeners();
}
// Close client
client.close();
if ((i + 1) % 3 === 0) {
forceGC();
const currentMemory = getMemoryUsage();
console.log(` Memory after closing ${i + 1} clients: ${currentMemory.heapUsed}MB`);
}
}
// Final memory check
forceGC();
await new Promise(resolve => setTimeout(resolve, 200));
const finalMemory = getMemoryUsage();
console.log('\n Event listener memory analysis:');
memorySnapshots.forEach(snapshot => {
const increase = snapshot.heap - initialMemory.heapUsed;
console.log(` ${snapshot.clientCount} clients: +${increase.toFixed(2)}MB`);
});
const finalIncrease = finalMemory.heap - initialMemory.heapUsed;
console.log(` Final memory after cleanup: +${finalIncrease.toFixed(2)}MB`);
console.log(` Event listener cleanup: ${finalIncrease < 1 ? 'Excellent' : 'Memory retained'}`);
} finally {
testServer.close();
}
});
console.log('\n✅ CREL-05: Memory Leak Prevention Reliability Tests completed');
console.log('🧠 All memory management scenarios tested successfully');
});

View File

@ -0,0 +1,547 @@
import { test } from '@git.zone/tstest/tapbundle';
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as crypto from 'crypto';
test('CREL-06: Concurrent Operation Safety Reliability Tests', async () => {
console.log('\n⚡ Testing SMTP Client Concurrent Operation Safety');
console.log('=' .repeat(60));
// Scenario 1: Simultaneous Connection Management
await test.test('Scenario 1: Simultaneous Connection Management', async () => {
console.log('\n🔗 Testing simultaneous connection management safety...');
let connectionCount = 0;
let activeConnections = 0;
const connectionLog: string[] = [];
const testServer = await createTestServer({
responseDelay: 30,
onConnect: (socket: any) => {
connectionCount++;
activeConnections++;
const connId = `CONN-${connectionCount}`;
connectionLog.push(`${new Date().toISOString()}: ${connId} OPENED (active: ${activeConnections})`);
console.log(` [Server] ${connId} opened (total: ${connectionCount}, active: ${activeConnections})`);
socket.on('close', () => {
activeConnections--;
connectionLog.push(`${new Date().toISOString()}: ${connId} CLOSED (active: ${activeConnections})`);
console.log(` [Server] ${connId} closed (active: ${activeConnections})`);
});
}
});
try {
console.log(' Creating multiple SMTP clients with shared connection pool...');
const clients = [];
for (let i = 0; i < 5; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 3,
maxMessages: 10,
connectionTimeout: 2000,
threadSafe: true
}));
}
console.log(' Launching concurrent email sending operations...');
const emailBatches = clients.map((client, clientIndex) => {
return Array.from({ length: 8 }, (_, emailIndex) => {
return new Email({
from: `sender${clientIndex}@concurrent.test`,
to: [`recipient${clientIndex}-${emailIndex}@concurrent.test`],
subject: `Concurrent Safety Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
text: `Testing concurrent operation safety from client ${clientIndex + 1}, email ${emailIndex + 1}`,
messageId: `concurrent-${clientIndex}-${emailIndex}@concurrent.test`
});
});
});
const startTime = Date.now();
const allPromises: Promise<any>[] = [];
// Launch all email operations simultaneously
emailBatches.forEach((emails, clientIndex) => {
emails.forEach((email, emailIndex) => {
const promise = clients[clientIndex].sendMail(email).then(result => {
console.log(` ✓ Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
return { success: true, clientIndex, emailIndex, result };
}).catch(error => {
console.log(` ✗ Client ${clientIndex + 1} Email ${emailIndex + 1} failed: ${error.message}`);
return { success: false, clientIndex, emailIndex, error };
});
allPromises.push(promise);
});
});
const results = await Promise.all(allPromises);
const endTime = Date.now();
// Close all clients
clients.forEach(client => client.close());
// Wait for connections to close
await new Promise(resolve => setTimeout(resolve, 500));
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
const totalEmails = emailBatches.flat().length;
console.log(`\n Concurrent operation results:`);
console.log(` Total operations: ${totalEmails}`);
console.log(` Successful: ${successful}, Failed: ${failed}`);
console.log(` Success rate: ${((successful / totalEmails) * 100).toFixed(1)}%`);
console.log(` Execution time: ${endTime - startTime}ms`);
console.log(` Peak connections: ${Math.max(...connectionLog.map(log => {
const match = log.match(/active: (\d+)/);
return match ? parseInt(match[1]) : 0;
}))}`);
console.log(` Connection management: ${activeConnections === 0 ? 'Clean' : 'Connections remaining'}`);
} finally {
testServer.close();
}
});
// Scenario 2: Thread-Safe Queue Operations
await test.test('Scenario 2: Thread-Safe Queue Operations', async () => {
console.log('\n🔒 Testing thread-safe queue operations...');
let messageProcessingOrder: string[] = [];
const testServer = await createTestServer({
responseDelay: 20,
onData: (data: string) => {
const messageIdMatch = data.match(/Message-ID:\s*<([^>]+)>/);
if (messageIdMatch) {
messageProcessingOrder.push(messageIdMatch[1]);
console.log(` [Server] Processing: ${messageIdMatch[1]}`);
}
}
});
try {
console.log(' Creating SMTP client with thread-safe queue...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
maxMessages: 50,
queueSafety: true,
lockingMode: 'strict'
});
console.log(' Launching concurrent queue operations...');
const operations: Promise<any>[] = [];
const emailGroups = ['A', 'B', 'C', 'D'];
// Create concurrent operations that modify the queue
emailGroups.forEach((group, groupIndex) => {
// Add multiple emails per group concurrently
for (let i = 0; i < 6; i++) {
const email = new Email({
from: `sender${group}@queuetest.example`,
to: [`recipient${group}${i}@queuetest.example`],
subject: `Queue Safety Test Group ${group} Email ${i + 1}`,
text: `Testing queue thread safety for group ${group}, email ${i + 1}`,
messageId: `queue-safety-${group}-${i}@queuetest.example`
});
const operation = smtpClient.sendMail(email).then(result => {
return {
success: true,
group,
index: i,
messageId: email.messageId,
timestamp: Date.now()
};
}).catch(error => {
return {
success: false,
group,
index: i,
messageId: email.messageId,
error: error.message
};
});
operations.push(operation);
}
});
const startTime = Date.now();
const results = await Promise.all(operations);
const endTime = Date.now();
// Wait for all processing to complete
await new Promise(resolve => setTimeout(resolve, 300));
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`\n Queue safety results:`);
console.log(` Total queue operations: ${operations.length}`);
console.log(` Successful: ${successful}, Failed: ${failed}`);
console.log(` Success rate: ${((successful / operations.length) * 100).toFixed(1)}%`);
console.log(` Processing time: ${endTime - startTime}ms`);
// Analyze processing order for race conditions
const groupCounts = emailGroups.reduce((acc, group) => {
acc[group] = messageProcessingOrder.filter(id => id.includes(`-${group}-`)).length;
return acc;
}, {} as Record<string, number>);
console.log(` Processing distribution:`);
Object.entries(groupCounts).forEach(([group, count]) => {
console.log(` Group ${group}: ${count} emails processed`);
});
const totalProcessed = Object.values(groupCounts).reduce((a, b) => a + b, 0);
console.log(` Queue integrity: ${totalProcessed === successful ? 'Maintained' : 'Potential race condition'}`);
smtpClient.close();
} finally {
testServer.close();
}
});
// Scenario 3: Concurrent Error Handling
await test.test('Scenario 3: Concurrent Error Handling', async () => {
console.log('\n❌ Testing concurrent error handling safety...');
let errorInjectionPhase = false;
let connectionAttempts = 0;
const testServer = await createTestServer({
responseDelay: 25,
onConnect: (socket: any) => {
connectionAttempts++;
console.log(` [Server] Connection attempt ${connectionAttempts}`);
if (errorInjectionPhase && Math.random() < 0.4) {
console.log(` [Server] Injecting connection error ${connectionAttempts}`);
socket.destroy();
return;
}
},
onData: (data: string, socket: any) => {
if (errorInjectionPhase && data.includes('MAIL FROM') && Math.random() < 0.3) {
console.log(' [Server] Injecting SMTP error');
socket.write('450 Temporary failure, please retry\r\n');
return false;
}
return true;
}
});
try {
console.log(' Creating multiple clients for concurrent error testing...');
const clients = [];
for (let i = 0; i < 4; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
retryDelay: 100,
retries: 3,
errorHandling: 'concurrent-safe',
failureRecovery: true
}));
}
const emails = [];
for (let clientIndex = 0; clientIndex < clients.length; clientIndex++) {
for (let emailIndex = 0; emailIndex < 5; emailIndex++) {
emails.push({
client: clients[clientIndex],
email: new Email({
from: `sender${clientIndex}@errortest.example`,
to: [`recipient${clientIndex}-${emailIndex}@errortest.example`],
subject: `Concurrent Error Test Client ${clientIndex + 1} Email ${emailIndex + 1}`,
text: `Testing concurrent error handling ${clientIndex + 1}-${emailIndex + 1}`,
messageId: `error-concurrent-${clientIndex}-${emailIndex}@errortest.example`
}),
clientIndex,
emailIndex
});
}
}
console.log(' Phase 1: Normal operation...');
const phase1Results = [];
const phase1Emails = emails.slice(0, 8); // First 8 emails
const phase1Promises = phase1Emails.map(({ client, email, clientIndex, emailIndex }) => {
return client.sendMail(email).then(result => {
console.log(` ✓ Phase 1: Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
return { success: true, phase: 1, clientIndex, emailIndex };
}).catch(error => {
console.log(` ✗ Phase 1: Client ${clientIndex + 1} Email ${emailIndex + 1} failed`);
return { success: false, phase: 1, clientIndex, emailIndex, error: error.message };
});
});
const phase1Resolved = await Promise.all(phase1Promises);
phase1Results.push(...phase1Resolved);
console.log(' Phase 2: Error injection enabled...');
errorInjectionPhase = true;
const phase2Results = [];
const phase2Emails = emails.slice(8); // Remaining emails
const phase2Promises = phase2Emails.map(({ client, email, clientIndex, emailIndex }) => {
return client.sendMail(email).then(result => {
console.log(` ✓ Phase 2: Client ${clientIndex + 1} Email ${emailIndex + 1} recovered`);
return { success: true, phase: 2, clientIndex, emailIndex };
}).catch(error => {
console.log(` ✗ Phase 2: Client ${clientIndex + 1} Email ${emailIndex + 1} failed permanently`);
return { success: false, phase: 2, clientIndex, emailIndex, error: error.message };
});
});
const phase2Resolved = await Promise.all(phase2Promises);
phase2Results.push(...phase2Resolved);
// Close all clients
clients.forEach(client => client.close());
const phase1Success = phase1Results.filter(r => r.success).length;
const phase2Success = phase2Results.filter(r => r.success).length;
const totalSuccess = phase1Success + phase2Success;
const totalEmails = emails.length;
console.log(`\n Concurrent error handling results:`);
console.log(` Phase 1 (normal): ${phase1Success}/${phase1Results.length} successful`);
console.log(` Phase 2 (errors): ${phase2Success}/${phase2Results.length} successful`);
console.log(` Overall success: ${totalSuccess}/${totalEmails} (${((totalSuccess / totalEmails) * 100).toFixed(1)}%)`);
console.log(` Error resilience: ${phase2Success > 0 ? 'Good' : 'Poor'}`);
console.log(` Concurrent error safety: ${phase1Success === phase1Results.length ? 'Maintained' : 'Compromised'}`);
} finally {
testServer.close();
}
});
// Scenario 4: Resource Contention Management
await test.test('Scenario 4: Resource Contention Management', async () => {
console.log('\n🏁 Testing resource contention management...');
const testServer = await createTestServer({
responseDelay: 40, // Slower responses to create contention
maxConnections: 3, // Limit server connections
onConnect: (socket: any) => {
console.log(' [Server] New connection established');
}
});
try {
console.log(' Creating high-contention scenario with limited resources...');
const clients = [];
// Create more clients than server can handle simultaneously
for (let i = 0; i < 8; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1, // Force contention
maxMessages: 10,
connectionTimeout: 3000,
resourceContention: 'managed',
backoffStrategy: 'exponential'
}));
}
const emails = [];
clients.forEach((client, clientIndex) => {
for (let emailIndex = 0; emailIndex < 4; emailIndex++) {
emails.push({
client,
email: new Email({
from: `sender${clientIndex}@contention.test`,
to: [`recipient${clientIndex}-${emailIndex}@contention.test`],
subject: `Resource Contention Test ${clientIndex + 1}-${emailIndex + 1}`,
text: `Testing resource contention management ${clientIndex + 1}-${emailIndex + 1}`,
messageId: `contention-${clientIndex}-${emailIndex}@contention.test`
}),
clientIndex,
emailIndex
});
}
});
console.log(' Launching high-contention operations...');
const startTime = Date.now();
const promises = emails.map(({ client, email, clientIndex, emailIndex }) => {
return client.sendMail(email).then(result => {
console.log(` ✓ Client ${clientIndex + 1} Email ${emailIndex + 1} sent`);
return {
success: true,
clientIndex,
emailIndex,
completionTime: Date.now() - startTime
};
}).catch(error => {
console.log(` ✗ Client ${clientIndex + 1} Email ${emailIndex + 1} failed: ${error.message}`);
return {
success: false,
clientIndex,
emailIndex,
error: error.message,
completionTime: Date.now() - startTime
};
});
});
const results = await Promise.all(promises);
const endTime = Date.now();
// Close all clients
clients.forEach(client => client.close());
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
const avgCompletionTime = results
.filter(r => r.success)
.reduce((sum, r) => sum + r.completionTime, 0) / successful || 0;
console.log(`\n Resource contention results:`);
console.log(` Total operations: ${emails.length}`);
console.log(` Successful: ${successful}, Failed: ${failed}`);
console.log(` Success rate: ${((successful / emails.length) * 100).toFixed(1)}%`);
console.log(` Total execution time: ${endTime - startTime}ms`);
console.log(` Average completion time: ${avgCompletionTime.toFixed(0)}ms`);
console.log(` Resource management: ${successful > emails.length * 0.8 ? 'Effective' : 'Needs improvement'}`);
} finally {
testServer.close();
}
});
// Scenario 5: Data Race Prevention
await test.test('Scenario 5: Data Race Prevention', async () => {
console.log('\n🏃 Testing data race prevention mechanisms...');
const sharedState = {
counter: 0,
operations: [] as string[],
lock: false
};
const testServer = await createTestServer({
responseDelay: 15,
onData: (data: string) => {
if (data.includes('Data Race Test')) {
// Simulate shared state access
if (!sharedState.lock) {
sharedState.lock = true;
sharedState.counter++;
sharedState.operations.push(`Operation ${sharedState.counter} at ${Date.now()}`);
sharedState.lock = false;
}
}
}
});
try {
console.log(' Setting up concurrent operations that access shared state...');
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 4,
racePreventionMode: true,
atomicOperations: true
});
const iterations = 20;
const concurrentOperations: Promise<any>[] = [];
console.log(' Launching concurrent operations...');
for (let i = 0; i < iterations; i++) {
const email = new Email({
from: 'sender@datarace.test',
to: [`recipient${i}@datarace.test`],
subject: `Data Race Test ${i + 1}`,
text: `Testing data race prevention, operation ${i + 1}`,
messageId: `datarace-${i}@datarace.test`
});
const operation = smtpClient.sendMail(email).then(result => {
return {
success: true,
operationId: i + 1,
messageId: result.messageId,
timestamp: Date.now()
};
}).catch(error => {
return {
success: false,
operationId: i + 1,
error: error.message,
timestamp: Date.now()
};
});
concurrentOperations.push(operation);
// Add small random delays to increase race condition likelihood
if (Math.random() < 0.3) {
await new Promise(resolve => setTimeout(resolve, 1));
}
}
const results = await Promise.all(concurrentOperations);
// Wait for shared state operations to complete
await new Promise(resolve => setTimeout(resolve, 200));
const successful = results.filter(r => r.success).length;
const failed = results.filter(r => !r.success).length;
console.log(`\n Data race prevention results:`);
console.log(` Concurrent operations: ${iterations}`);
console.log(` Successful: ${successful}, Failed: ${failed}`);
console.log(` Success rate: ${((successful / iterations) * 100).toFixed(1)}%`);
console.log(` Shared state counter: ${sharedState.counter}`);
console.log(` State operations recorded: ${sharedState.operations.length}`);
console.log(` Data consistency: ${sharedState.counter === sharedState.operations.length ? 'Maintained' : 'Race condition detected'}`);
console.log(` Race prevention: ${sharedState.counter <= successful ? 'Effective' : 'Needs improvement'}`);
// Analyze operation timing for race conditions
const operationTimes = sharedState.operations.map(op => {
const match = op.match(/at (\d+)/);
return match ? parseInt(match[1]) : 0;
});
if (operationTimes.length > 1) {
const timeGaps = [];
for (let i = 1; i < operationTimes.length; i++) {
timeGaps.push(operationTimes[i] - operationTimes[i - 1]);
}
const avgGap = timeGaps.reduce((a, b) => a + b, 0) / timeGaps.length;
console.log(` Average operation gap: ${avgGap.toFixed(1)}ms`);
console.log(` Timing consistency: ${avgGap > 0 ? 'Sequential' : 'Potential overlap'}`);
}
smtpClient.close();
} finally {
testServer.close();
}
});
console.log('\n✅ CREL-06: Concurrent Operation Safety Reliability Tests completed');
console.log('⚡ All concurrency safety scenarios tested successfully');
});

View File

@ -0,0 +1,585 @@
import { test } from '@git.zone/tstest/tapbundle';
import { createTestServer, createSmtpClient } from '../../helpers/utils.js';
import { Email } from '../../../ts/mail/core/classes.email.js';
import * as fs from 'fs';
import * as path from 'path';
test('CREL-07: Resource Cleanup Reliability Tests', async () => {
console.log('\n🧹 Testing SMTP Client Resource Cleanup Reliability');
console.log('=' .repeat(60));
const tempDir = path.join(process.cwd(), '.nogit', 'test-resource-cleanup');
// Ensure test directory exists
if (!fs.existsSync(tempDir)) {
fs.mkdirSync(tempDir, { recursive: true });
}
// Helper function to count active resources
const getResourceCounts = () => {
const usage = process.memoryUsage();
return {
memory: Math.round(usage.heapUsed / 1024 / 1024 * 100) / 100, // MB
handles: process._getActiveHandles ? process._getActiveHandles().length : 0,
requests: process._getActiveRequests ? process._getActiveRequests().length : 0
};
};
// Scenario 1: Connection Pool Cleanup
await test.test('Scenario 1: Connection Pool Cleanup', async () => {
console.log('\n🏊 Testing connection pool resource cleanup...');
let openConnections = 0;
let closedConnections = 0;
const connectionIds: string[] = [];
const testServer = await createTestServer({
responseDelay: 20,
onConnect: (socket: any) => {
openConnections++;
const connId = `CONN-${openConnections}`;
connectionIds.push(connId);
console.log(` [Server] ${connId} opened (total open: ${openConnections})`);
socket.on('close', () => {
closedConnections++;
console.log(` [Server] Connection closed (total closed: ${closedConnections})`);
});
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Phase 1: Creating and using connection pools...');
const clients = [];
for (let poolIndex = 0; poolIndex < 4; poolIndex++) {
console.log(` Creating connection pool ${poolIndex + 1}...`);
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 3,
maxMessages: 20,
resourceCleanup: true,
autoCleanupInterval: 1000
});
clients.push(smtpClient);
// Send emails through this pool
const emails = [];
for (let i = 0; i < 5; i++) {
emails.push(new Email({
from: `sender${poolIndex}@cleanup.test`,
to: [`recipient${i}@cleanup.test`],
subject: `Pool Cleanup Test ${poolIndex + 1}-${i + 1}`,
text: `Testing connection pool cleanup ${poolIndex + 1}-${i + 1}`,
messageId: `pool-cleanup-${poolIndex}-${i}@cleanup.test`
}));
}
const promises = emails.map((email, index) => {
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Pool ${poolIndex + 1} Email ${index + 1} sent`);
return { success: true };
}).catch(error => {
console.log(` ✗ Pool ${poolIndex + 1} Email ${index + 1} failed`);
return { success: false, error };
});
});
const results = await Promise.all(promises);
const successful = results.filter(r => r.success).length;
console.log(` Pool ${poolIndex + 1}: ${successful}/${emails.length} emails sent`);
}
const afterCreation = getResourceCounts();
console.log(` After pool creation: ${afterCreation.memory}MB memory, ${afterCreation.handles} handles`);
console.log(' Phase 2: Closing all pools and testing cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing pool ${i + 1}...`);
clients[i].close();
// Wait for cleanup to occur
await new Promise(resolve => setTimeout(resolve, 200));
const currentResources = getResourceCounts();
console.log(` Resources after closing pool ${i + 1}: ${currentResources.memory}MB, ${currentResources.handles} handles`);
}
// Wait for all cleanup to complete
await new Promise(resolve => setTimeout(resolve, 1000));
const finalResources = getResourceCounts();
console.log(`\n Resource cleanup assessment:`);
console.log(` Initial: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(` Final: ${finalResources.memory}MB memory, ${finalResources.handles} handles`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Good' : 'Memory retained'}`);
console.log(` Handle cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Good' : 'Handles remaining'}`);
console.log(` Connection cleanup: ${closedConnections >= openConnections - 1 ? 'Complete' : 'Incomplete'}`);
} finally {
testServer.close();
}
});
// Scenario 2: File Handle and Stream Cleanup
await test.test('Scenario 2: File Handle and Stream Cleanup', async () => {
console.log('\n📁 Testing file handle and stream cleanup...');
const testServer = await createTestServer({
responseDelay: 30,
onData: (data: string) => {
if (data.includes('Attachment Test')) {
console.log(' [Server] Processing attachment email');
}
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating temporary files for attachment testing...');
const tempFiles: string[] = [];
for (let i = 0; i < 8; i++) {
const fileName = path.join(tempDir, `attachment-${i}.txt`);
const content = `Attachment content ${i + 1}\n${'X'.repeat(1000)}`; // 1KB files
fs.writeFileSync(fileName, content);
tempFiles.push(fileName);
console.log(` Created temp file: ${fileName}`);
}
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
streamCleanup: true,
fileHandleManagement: true
});
console.log(' Sending emails with file attachments...');
const emailPromises = tempFiles.map((filePath, index) => {
const email = new Email({
from: 'sender@filehandle.test',
to: [`recipient${index}@filehandle.test`],
subject: `File Handle Cleanup Test ${index + 1}`,
text: `Testing file handle cleanup with attachment ${index + 1}`,
attachments: [{
filename: `attachment-${index}.txt`,
path: filePath
}],
messageId: `filehandle-${index}@filehandle.test`
});
return smtpClient.sendMail(email).then(result => {
console.log(` ✓ Email ${index + 1} with attachment sent`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Email ${index + 1} failed: ${error.message}`);
return { success: false, index, error };
});
});
const results = await Promise.all(emailPromises);
const successful = results.filter(r => r.success).length;
console.log(` Email sending completed: ${successful}/${tempFiles.length} successful`);
const afterSending = getResourceCounts();
console.log(` Resources after sending: ${afterSending.memory}MB memory, ${afterSending.handles} handles`);
console.log(' Closing client and testing file handle cleanup...');
smtpClient.close();
// Wait for cleanup
await new Promise(resolve => setTimeout(resolve, 500));
// Clean up temp files
tempFiles.forEach(filePath => {
try {
fs.unlinkSync(filePath);
console.log(` Cleaned up: ${filePath}`);
} catch (error) {
console.log(` Failed to clean up ${filePath}: ${error.message}`);
}
});
const finalResources = getResourceCounts();
console.log(`\n File handle cleanup assessment:`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` After sending: ${afterSending.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Handle management: ${finalResources.handles <= initialResources.handles + 2 ? 'Effective' : 'Handles leaked'}`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 3 ? 'Good' : 'Memory retained'}`);
} finally {
testServer.close();
}
});
// Scenario 3: Timer and Interval Cleanup
await test.test('Scenario 3: Timer and Interval Cleanup', async () => {
console.log('\n⏰ Testing timer and interval cleanup...');
const testServer = await createTestServer({
responseDelay: 40
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating clients with various timer configurations...');
const clients = [];
for (let i = 0; i < 3; i++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
// Timer configurations
connectionTimeout: 2000,
keepAlive: true,
keepAliveInterval: 1000,
retryDelay: 500,
healthCheckInterval: 800,
timerCleanup: true
});
clients.push(smtpClient);
// Send an email to activate timers
const email = new Email({
from: `sender${i}@timer.test`,
to: [`recipient${i}@timer.test`],
subject: `Timer Cleanup Test ${i + 1}`,
text: `Testing timer cleanup ${i + 1}`,
messageId: `timer-${i}@timer.test`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${i + 1} email sent (timers active)`);
} catch (error) {
console.log(` ✗ Client ${i + 1} failed: ${error.message}`);
}
}
// Let timers run for a while
console.log(' Allowing timers to run...');
await new Promise(resolve => setTimeout(resolve, 2000));
const withTimers = getResourceCounts();
console.log(` Resources with active timers: ${withTimers.memory}MB memory, ${withTimers.handles} handles`);
console.log(' Closing clients and testing timer cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1}...`);
clients[i].close();
// Wait for timer cleanup
await new Promise(resolve => setTimeout(resolve, 300));
const currentResources = getResourceCounts();
console.log(` Resources after closing client ${i + 1}: ${currentResources.handles} handles`);
}
// Wait for all timer cleanup to complete
await new Promise(resolve => setTimeout(resolve, 1500));
const finalResources = getResourceCounts();
console.log(`\n Timer cleanup assessment:`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` With timers: ${withTimers.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Timer cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Timers remaining'}`);
console.log(` Resource management: ${finalResources.handles < withTimers.handles ? 'Effective' : 'Incomplete'}`);
} finally {
testServer.close();
}
});
// Scenario 4: Event Listener and Callback Cleanup
await test.test('Scenario 4: Event Listener and Callback Cleanup', async () => {
console.log('\n🎧 Testing event listener and callback cleanup...');
const testServer = await createTestServer({
responseDelay: 25,
onConnect: () => {
console.log(' [Server] Connection for event cleanup test');
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory`);
console.log(' Creating clients with extensive event listeners...');
const clients = [];
const eventHandlers: any[] = [];
for (let clientIndex = 0; clientIndex < 5; clientIndex++) {
const smtpClient = createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 1,
eventCleanup: true
});
clients.push(smtpClient);
// Add multiple event listeners
const handlers = [];
for (let eventIndex = 0; eventIndex < 6; eventIndex++) {
const handler = (data: any) => {
// Event handler with closure
console.log(` Event ${eventIndex} from client ${clientIndex}: ${typeof data}`);
};
handlers.push(handler);
// Add event listeners (simulated)
if (smtpClient.on) {
smtpClient.on('connect', handler);
smtpClient.on('error', handler);
smtpClient.on('close', handler);
smtpClient.on('data', handler);
}
}
eventHandlers.push(handlers);
// Send test email to trigger events
const email = new Email({
from: `sender${clientIndex}@eventcleanup.test`,
to: [`recipient${clientIndex}@eventcleanup.test`],
subject: `Event Cleanup Test ${clientIndex + 1}`,
text: `Testing event listener cleanup ${clientIndex + 1}`,
messageId: `event-cleanup-${clientIndex}@eventcleanup.test`
});
try {
await smtpClient.sendMail(email);
console.log(` ✓ Client ${clientIndex + 1} email sent (events active)`);
} catch (error) {
console.log(` ✗ Client ${clientIndex + 1} failed: ${error.message}`);
}
}
const withEvents = getResourceCounts();
console.log(` Resources with event listeners: ${withEvents.memory}MB memory`);
console.log(' Closing clients and testing event listener cleanup...');
for (let i = 0; i < clients.length; i++) {
console.log(` Closing client ${i + 1} and removing ${eventHandlers[i].length} event listeners...`);
// Remove event listeners manually first
if (clients[i].removeAllListeners) {
clients[i].removeAllListeners();
}
// Close client
clients[i].close();
// Clear handler references
eventHandlers[i].length = 0;
await new Promise(resolve => setTimeout(resolve, 100));
}
// Force garbage collection if available
if (global.gc) {
global.gc();
global.gc();
}
await new Promise(resolve => setTimeout(resolve, 500));
const finalResources = getResourceCounts();
console.log(`\n Event listener cleanup assessment:`);
console.log(` Initial memory: ${initialResources.memory}MB`);
console.log(` With events: ${withEvents.memory}MB`);
console.log(` Final memory: ${finalResources.memory}MB`);
console.log(` Memory cleanup: ${finalResources.memory - initialResources.memory < 2 ? 'Effective' : 'Memory retained'}`);
console.log(` Event cleanup: ${finalResources.memory < withEvents.memory ? 'Successful' : 'Partial'}`);
} finally {
testServer.close();
}
});
// Scenario 5: Error State Cleanup
await test.test('Scenario 5: Error State Cleanup', async () => {
console.log('\n💥 Testing error state cleanup...');
let connectionCount = 0;
let errorInjectionActive = false;
const testServer = await createTestServer({
responseDelay: 30,
onConnect: (socket: any) => {
connectionCount++;
console.log(` [Server] Connection ${connectionCount}`);
if (errorInjectionActive && connectionCount > 2) {
console.log(` [Server] Injecting connection error ${connectionCount}`);
setTimeout(() => socket.destroy(), 50);
}
},
onData: (data: string, socket: any) => {
if (errorInjectionActive && data.includes('MAIL FROM')) {
socket.write('500 Internal server error\r\n');
return false;
}
return true;
}
});
try {
const initialResources = getResourceCounts();
console.log(` Initial resources: ${initialResources.memory}MB memory, ${initialResources.handles} handles`);
console.log(' Creating clients for error state testing...');
const clients = [];
for (let i = 0; i < 4; i++) {
clients.push(createSmtpClient({
host: testServer.hostname,
port: testServer.port,
secure: false,
pool: true,
maxConnections: 2,
retryDelay: 200,
retries: 2,
errorStateCleanup: true,
gracefulErrorHandling: true
}));
}
console.log(' Phase 1: Normal operation...');
const email1 = new Email({
from: 'sender@errorstate.test',
to: ['recipient1@errorstate.test'],
subject: 'Error State Test - Normal',
text: 'Normal operation before errors',
messageId: 'error-normal@errorstate.test'
});
try {
await clients[0].sendMail(email1);
console.log(' ✓ Normal operation successful');
} catch (error) {
console.log(' ✗ Normal operation failed');
}
const afterNormal = getResourceCounts();
console.log(` Resources after normal operation: ${afterNormal.handles} handles`);
console.log(' Phase 2: Error injection phase...');
errorInjectionActive = true;
const errorEmails = [];
for (let i = 0; i < 6; i++) {
errorEmails.push(new Email({
from: 'sender@errorstate.test',
to: [`recipient${i}@errorstate.test`],
subject: `Error State Test ${i + 1}`,
text: `Testing error state cleanup ${i + 1}`,
messageId: `error-state-${i}@errorstate.test`
}));
}
const errorPromises = errorEmails.map((email, index) => {
const client = clients[index % clients.length];
return client.sendMail(email).then(result => {
console.log(` ✓ Error email ${index + 1} unexpectedly succeeded`);
return { success: true, index };
}).catch(error => {
console.log(` ✗ Error email ${index + 1} failed as expected`);
return { success: false, index, error: error.message };
});
});
const errorResults = await Promise.all(errorPromises);
const afterErrors = getResourceCounts();
console.log(` Resources after error phase: ${afterErrors.handles} handles`);
console.log(' Phase 3: Recovery and cleanup...');
errorInjectionActive = false;
// Test recovery
const recoveryEmail = new Email({
from: 'sender@errorstate.test',
to: ['recovery@errorstate.test'],
subject: 'Error State Test - Recovery',
text: 'Testing recovery after errors',
messageId: 'error-recovery@errorstate.test'
});
try {
await clients[0].sendMail(recoveryEmail);
console.log(' ✓ Recovery successful');
} catch (error) {
console.log(' ✗ Recovery failed');
}
console.log(' Closing all clients...');
clients.forEach((client, index) => {
console.log(` Closing client ${index + 1}...`);
client.close();
});
await new Promise(resolve => setTimeout(resolve, 1000));
const finalResources = getResourceCounts();
const errorSuccessful = errorResults.filter(r => r.success).length;
const errorFailed = errorResults.filter(r => !r.success).length;
console.log(`\n Error state cleanup assessment:`);
console.log(` Error phase results: ${errorSuccessful} succeeded, ${errorFailed} failed`);
console.log(` Initial handles: ${initialResources.handles}`);
console.log(` After errors: ${afterErrors.handles}`);
console.log(` Final handles: ${finalResources.handles}`);
console.log(` Error state cleanup: ${finalResources.handles <= initialResources.handles + 1 ? 'Complete' : 'Incomplete'}`);
console.log(` Recovery capability: ${errorFailed > 0 ? 'Error handling active' : 'No errors detected'}`);
console.log(` Resource management: ${finalResources.handles < afterErrors.handles ? 'Effective' : 'Needs improvement'}`);
} finally {
testServer.close();
}
});
// Cleanup test directory
try {
if (fs.existsSync(tempDir)) {
const files = fs.readdirSync(tempDir);
for (const file of files) {
const filePath = path.join(tempDir, file);
if (fs.existsSync(filePath)) {
fs.unlinkSync(filePath);
}
}
fs.rmdirSync(tempDir);
console.log('\n🧹 Test directory cleaned up successfully');
}
} catch (error) {
console.log(`\n⚠ Warning: Could not clean up test directory: ${error.message}`);
}
console.log('\n✅ CREL-07: Resource Cleanup Reliability Tests completed');
console.log('🧹 All resource cleanup scenarios tested successfully');
});