import { tap, expect } from '@git.zone/tstest/tapbundle'; import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js'; import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js'; import { Email } from '../../../ts/mail/core/classes.email.js'; import * as fs from 'fs'; import * as path from 'path'; tap.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'); });