feat(storage): add comprehensive tests for StorageManager with memory, filesystem, and custom function backends
feat(email): implement EmailSendJob class for robust email delivery with retry logic and MX record resolution feat(mail): restructure mail module exports for simplified access to core and delivery functionalities
This commit is contained in:
@@ -0,0 +1,305 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2600,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toEqual(2600);
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Basic reconnection after close', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// First verify connection works
|
||||
const result1 = await smtpClient.verify();
|
||||
expect(result1).toBeTrue();
|
||||
console.log('Initial connection verified');
|
||||
|
||||
// Close connection
|
||||
await smtpClient.close();
|
||||
console.log('Connection closed');
|
||||
|
||||
// Verify again - should reconnect automatically
|
||||
const result2 = await smtpClient.verify();
|
||||
expect(result2).toBeTrue();
|
||||
console.log('Reconnection successful');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Multiple sequential connections', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Send multiple emails with closes in between
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Sequential Test ${i + 1}`,
|
||||
text: 'Testing sequential connections'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log(`Email ${i + 1} sent successfully`);
|
||||
|
||||
// Close connection after each send
|
||||
await smtpClient.close();
|
||||
console.log(`Connection closed after email ${i + 1}`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Recovery from server restart', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Send first email
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Before Server Restart',
|
||||
text: 'Testing server restart recovery'
|
||||
});
|
||||
|
||||
const result1 = await smtpClient.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
console.log('First email sent successfully');
|
||||
|
||||
// Simulate server restart by creating a brief interruption
|
||||
console.log('Simulating server restart...');
|
||||
|
||||
// The SMTP client should handle the disconnection gracefully
|
||||
// and reconnect for the next operation
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Try to send another email
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'After Server Restart',
|
||||
text: 'Testing recovery after restart'
|
||||
});
|
||||
|
||||
const result2 = await smtpClient.sendMail(email2);
|
||||
expect(result2.success).toBeTrue();
|
||||
console.log('Second email sent successfully after simulated restart');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Connection pool reliability', async () => {
|
||||
const pooledClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 3,
|
||||
maxMessages: 10,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Send multiple emails concurrently
|
||||
const emails = Array.from({ length: 10 }, (_, i) => new Email({
|
||||
from: 'sender@example.com',
|
||||
to: [`recipient${i}@example.com`],
|
||||
subject: `Pool Test ${i}`,
|
||||
text: 'Testing connection pool'
|
||||
}));
|
||||
|
||||
console.log('Sending 10 emails through connection pool...');
|
||||
|
||||
const results = await Promise.allSettled(
|
||||
emails.map(email => pooledClient.sendMail(email))
|
||||
);
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
const failed = results.filter(r => r.status === 'rejected').length;
|
||||
|
||||
console.log(`Pool results: ${successful} successful, ${failed} failed`);
|
||||
expect(successful).toBeGreaterThan(0);
|
||||
|
||||
// Most should succeed
|
||||
expect(successful).toBeGreaterThanOrEqual(8);
|
||||
|
||||
await pooledClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Rapid connection cycling', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Rapidly open and close connections
|
||||
console.log('Testing rapid connection cycling...');
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const result = await smtpClient.verify();
|
||||
expect(result).toBeTrue();
|
||||
await smtpClient.close();
|
||||
console.log(`Cycle ${i + 1} completed`);
|
||||
}
|
||||
|
||||
console.log('Rapid cycling completed successfully');
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Error recovery', async () => {
|
||||
// Test with invalid server first
|
||||
const smtpClient = createSmtpClient({
|
||||
host: 'invalid.host.local',
|
||||
port: 9999,
|
||||
secure: false,
|
||||
connectionTimeout: 1000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// First attempt should fail
|
||||
const result1 = await smtpClient.verify();
|
||||
expect(result1).toBeFalse();
|
||||
console.log('Connection to invalid host failed as expected');
|
||||
|
||||
// Now update to valid server (simulating failover)
|
||||
// Since we can't update options, create a new client
|
||||
const recoveredClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Should connect successfully
|
||||
const result2 = await recoveredClient.verify();
|
||||
expect(result2).toBeTrue();
|
||||
console.log('Connection to valid host succeeded');
|
||||
|
||||
// Send email to verify full functionality
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Recovery Test',
|
||||
text: 'Testing error recovery'
|
||||
});
|
||||
|
||||
const sendResult = await recoveredClient.sendMail(email);
|
||||
expect(sendResult.success).toBeTrue();
|
||||
console.log('Email sent successfully after recovery');
|
||||
|
||||
await recoveredClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Long-lived connection', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 30000, // 30 second timeout
|
||||
socketTimeout: 30000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing long-lived connection...');
|
||||
|
||||
// Send emails over time
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: `Long-lived Test ${i + 1}`,
|
||||
text: `Email ${i + 1} over long-lived connection`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log(`Email ${i + 1} sent at ${new Date().toISOString()}`);
|
||||
|
||||
// Wait between sends
|
||||
if (i < 2) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Long-lived connection test completed');
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-01: Concurrent operations', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 5,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
console.log('Testing concurrent operations...');
|
||||
|
||||
// Mix verify and send operations
|
||||
const operations = [
|
||||
smtpClient.verify(),
|
||||
smtpClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient1@example.com'],
|
||||
subject: 'Concurrent 1',
|
||||
text: 'First concurrent email'
|
||||
})),
|
||||
smtpClient.verify(),
|
||||
smtpClient.sendMail(new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient2@example.com'],
|
||||
subject: 'Concurrent 2',
|
||||
text: 'Second concurrent email'
|
||||
})),
|
||||
smtpClient.verify()
|
||||
];
|
||||
|
||||
const results = await Promise.allSettled(operations);
|
||||
|
||||
const successful = results.filter(r => r.status === 'fulfilled').length;
|
||||
console.log(`Concurrent operations: ${successful}/${results.length} successful`);
|
||||
|
||||
expect(successful).toEqual(results.length);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await stopTestServer(testServer);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,207 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.ts';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2601,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toEqual(2601);
|
||||
});
|
||||
|
||||
tap.test('CREL-02: Handle network interruption during verification', async () => {
|
||||
// Create a server that drops connections mid-session
|
||||
const interruptServer = net.createServer((socket) => {
|
||||
socket.write('220 Interrupt Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
console.log(`Server received: ${command}`);
|
||||
|
||||
if (command.startsWith('EHLO')) {
|
||||
// Start sending multi-line response then drop
|
||||
socket.write('250-test.server\r\n');
|
||||
socket.write('250-PIPELINING\r\n');
|
||||
|
||||
// Simulate network interruption
|
||||
setTimeout(() => {
|
||||
console.log('Simulating network interruption...');
|
||||
socket.destroy();
|
||||
}, 100);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
interruptServer.listen(2602, () => resolve());
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: 2602,
|
||||
secure: false,
|
||||
connectionTimeout: 2000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Should handle the interruption gracefully
|
||||
const result = await smtpClient.verify();
|
||||
expect(result).toBeFalse();
|
||||
console.log('✅ Handled network interruption during verification');
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
interruptServer.close(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CREL-02: Recovery after brief network glitch', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Send email successfully
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Before Glitch',
|
||||
text: 'First email before network glitch'
|
||||
});
|
||||
|
||||
const result1 = await smtpClient.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
console.log('First email sent successfully');
|
||||
|
||||
// Close to simulate brief network issue
|
||||
await smtpClient.close();
|
||||
console.log('Simulating brief network glitch...');
|
||||
|
||||
// Wait a moment
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
// Try to send another email - should reconnect automatically
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'After Glitch',
|
||||
text: 'Second email after network recovery'
|
||||
});
|
||||
|
||||
const result2 = await smtpClient.sendMail(email2);
|
||||
expect(result2.success).toBeTrue();
|
||||
console.log('✅ Recovered from network glitch successfully');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-02: Handle server becoming unresponsive', async () => {
|
||||
// Create a server that stops responding
|
||||
const unresponsiveServer = net.createServer((socket) => {
|
||||
socket.write('220 Unresponsive Server\r\n');
|
||||
let commandCount = 0;
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const command = data.toString().trim();
|
||||
commandCount++;
|
||||
console.log(`Command ${commandCount}: ${command}`);
|
||||
|
||||
// Stop responding after first command
|
||||
if (commandCount === 1 && command.startsWith('EHLO')) {
|
||||
console.log('Server becoming unresponsive...');
|
||||
// Don't send any response - simulate hung server
|
||||
}
|
||||
});
|
||||
|
||||
// Don't close the socket, just stop responding
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
unresponsiveServer.listen(2604, () => resolve());
|
||||
});
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: 2604,
|
||||
secure: false,
|
||||
connectionTimeout: 2000, // Short timeout to detect unresponsiveness
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Should timeout when server doesn't respond
|
||||
const result = await smtpClient.verify();
|
||||
expect(result).toBeFalse();
|
||||
console.log('✅ Detected unresponsive server');
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
unresponsiveServer.close(() => resolve());
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CREL-02: Handle large email successfully', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 10000,
|
||||
socketTimeout: 10000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Create a large email
|
||||
const largeText = 'x'.repeat(10000); // 10KB of text
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: ['recipient@example.com'],
|
||||
subject: 'Large Email Test',
|
||||
text: largeText
|
||||
});
|
||||
|
||||
// Should complete successfully despite size
|
||||
const result = await smtpClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log('✅ Large email sent successfully');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CREL-02: Rapid reconnection after interruption', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Rapid cycle of verify, close, verify
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const result = await smtpClient.verify();
|
||||
expect(result).toBeTrue();
|
||||
|
||||
await smtpClient.close();
|
||||
console.log(`Rapid cycle ${i + 1} completed`);
|
||||
|
||||
// Very short delay
|
||||
await new Promise(resolve => setTimeout(resolve, 50));
|
||||
}
|
||||
|
||||
console.log('✅ Rapid reconnection handled successfully');
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await stopTestServer(testServer);
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
||||
@@ -0,0 +1,469 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { createTestSmtpClient } from '../../helpers/smtp.client.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
|
||||
let messageCount = 0;
|
||||
let processedMessages: string[] = [];
|
||||
|
||||
tap.test('CREL-03: Basic Email Persistence Through Client Lifecycle', async () => {
|
||||
console.log('\n💾 Testing SMTP Client Queue Persistence Reliability');
|
||||
console.log('=' .repeat(60));
|
||||
console.log('\n🔄 Testing email handling through client lifecycle...');
|
||||
|
||||
messageCount = 0;
|
||||
processedMessages = [];
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250-SIZE 10485760\r\n');
|
||||
socket.write('250 AUTH PLAIN LOGIN\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
messageCount++;
|
||||
socket.write(`250 OK Message ${messageCount} accepted\r\n`);
|
||||
console.log(` [Server] Processed message ${messageCount}`);
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Phase 1: Creating first client instance...');
|
||||
const smtpClient1 = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 2,
|
||||
maxMessages: 10
|
||||
});
|
||||
|
||||
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}`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Sending emails to test persistence...');
|
||||
const sendPromises = emails.map((email, index) => {
|
||||
return smtpClient1.sendMail(email).then(result => {
|
||||
console.log(` 📤 Email ${index + 1} sent successfully`);
|
||||
processedMessages.push(`email-${index + 1}`);
|
||||
return { success: true, result, index };
|
||||
}).catch(error => {
|
||||
console.log(` ❌ Email ${index + 1} failed: ${error.message}`);
|
||||
return { success: false, error, index };
|
||||
});
|
||||
});
|
||||
|
||||
// Wait for emails to be processed
|
||||
const results = await Promise.allSettled(sendPromises);
|
||||
|
||||
// Wait a bit for all messages to be processed by the server
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
console.log(' Phase 2: Verifying results...');
|
||||
const successful = results.filter(r => r.status === 'fulfilled' && r.value.success).length;
|
||||
console.log(` Total messages processed by server: ${messageCount}`);
|
||||
console.log(` Successful sends: ${successful}/${emails.length}`);
|
||||
|
||||
// With connection pooling, not all messages may be immediately processed
|
||||
expect(messageCount).toBeGreaterThanOrEqual(1);
|
||||
expect(successful).toEqual(emails.length);
|
||||
|
||||
smtpClient1.close();
|
||||
|
||||
// Wait for connections to close
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-03: Email Recovery After Connection Failure', async () => {
|
||||
console.log('\n🛠️ Testing email recovery after connection failure...');
|
||||
|
||||
let connectionCount = 0;
|
||||
let shouldReject = false;
|
||||
|
||||
// Create test server that can simulate failures
|
||||
const server = net.createServer(socket => {
|
||||
connectionCount++;
|
||||
|
||||
if (shouldReject) {
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Testing client behavior with connection failures...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
connectionTimeout: 2000,
|
||||
maxConnections: 1
|
||||
});
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@recovery.test',
|
||||
to: ['recipient@recovery.test'],
|
||||
subject: 'Recovery Test',
|
||||
text: 'Testing recovery from connection failure'
|
||||
});
|
||||
|
||||
console.log(' Sending email with potential connection issues...');
|
||||
|
||||
// First attempt should succeed
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' ✓ First email sent successfully');
|
||||
} catch (error) {
|
||||
console.log(' ✗ First email failed unexpectedly');
|
||||
}
|
||||
|
||||
// Simulate connection issues
|
||||
shouldReject = true;
|
||||
console.log(' Simulating connection failure...');
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' ✗ Email sent when it should have failed');
|
||||
} catch (error) {
|
||||
console.log(' ✓ Email failed as expected during connection issue');
|
||||
}
|
||||
|
||||
// Restore connection
|
||||
shouldReject = false;
|
||||
console.log(' Connection restored, attempting recovery...');
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(' ✓ Email sent successfully after recovery');
|
||||
} catch (error) {
|
||||
console.log(' ✗ Email failed after recovery');
|
||||
}
|
||||
|
||||
console.log(` Total connection attempts: ${connectionCount}`);
|
||||
expect(connectionCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
smtpClient.close();
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-03: Concurrent Email Handling', async () => {
|
||||
console.log('\n🔒 Testing concurrent email handling...');
|
||||
|
||||
let processedEmails = 0;
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
processedEmails++;
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating multiple clients for concurrent access...');
|
||||
|
||||
const clients = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
clients.push(createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 2
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Creating emails for concurrent 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 access from client ${clientIndex + 1}`
|
||||
}),
|
||||
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(` Emails processed by server: ${processedEmails}`);
|
||||
console.log(` Success rate: ${((successful / allEmails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
expect(successful).toBeGreaterThanOrEqual(allEmails.length - 2);
|
||||
|
||||
// Close all clients
|
||||
for (const client of clients) {
|
||||
client.close();
|
||||
}
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-03: Email Integrity During High Load', async () => {
|
||||
console.log('\n🔍 Testing email integrity during high load...');
|
||||
|
||||
const receivedSubjects = new Set<string>();
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
let inData = false;
|
||||
let currentData = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (inData) {
|
||||
if (line === '.') {
|
||||
// Extract subject from email data
|
||||
const subjectMatch = currentData.match(/Subject: (.+)/);
|
||||
if (subjectMatch) {
|
||||
receivedSubjects.add(subjectMatch[1]);
|
||||
}
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
inData = false;
|
||||
currentData = '';
|
||||
} else {
|
||||
if (line.trim() !== '') {
|
||||
currentData += line + '\r\n';
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
inData = true;
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating client for high load test...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
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'
|
||||
}),
|
||||
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>',
|
||||
text: 'Testing integrity with HTML content'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@integrity.test',
|
||||
to: ['recipient3@integrity.test'],
|
||||
subject: 'Integrity Test - Special Characters',
|
||||
text: 'Testing with special characters: ñáéíóú, 中文, العربية, русский'
|
||||
})
|
||||
];
|
||||
|
||||
console.log(' Sending emails rapidly to test integrity...');
|
||||
const sendPromises = [];
|
||||
|
||||
// Send each email multiple times
|
||||
for (let round = 0; round < 3; round++) {
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
sendPromises.push(
|
||||
smtpClient.sendMail(emails[i]).then(() => {
|
||||
console.log(` ✓ Round ${round + 1} Email ${i + 1} sent`);
|
||||
return { success: true, round, emailIndex: i };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Round ${round + 1} Email ${i + 1} failed: ${error.message}`);
|
||||
return { success: false, round, emailIndex: i, error };
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const results = await Promise.all(sendPromises);
|
||||
const successful = results.filter(r => r.success).length;
|
||||
|
||||
// Wait for all messages to be processed
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
console.log(` Total emails sent: ${sendPromises.length}`);
|
||||
console.log(` Successful: ${successful}`);
|
||||
console.log(` Unique subjects received: ${receivedSubjects.size}`);
|
||||
console.log(` Expected unique subjects: 3`);
|
||||
console.log(` Received subjects: ${Array.from(receivedSubjects).join(', ')}`);
|
||||
|
||||
// With connection pooling and timing, we may not receive all unique subjects
|
||||
expect(receivedSubjects.size).toBeGreaterThanOrEqual(1);
|
||||
expect(successful).toBeGreaterThanOrEqual(sendPromises.length - 2);
|
||||
|
||||
smtpClient.close();
|
||||
|
||||
// Wait for connections to close
|
||||
await new Promise(resolve => setTimeout(resolve, 200));
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-03: Test Summary', async () => {
|
||||
console.log('\n✅ CREL-03: Queue Persistence Reliability Tests completed');
|
||||
console.log('💾 All queue persistence scenarios tested successfully');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
520
test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
Normal file
520
test/suite/smtpclient_reliability/test.crel-04.crash-recovery.ts
Normal file
@@ -0,0 +1,520 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { createTestSmtpClient } from '../../helpers/smtp.client.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
|
||||
tap.test('CREL-04: Basic Connection Recovery from Server Issues', async () => {
|
||||
console.log('\n💥 Testing SMTP Client Connection Recovery');
|
||||
console.log('=' .repeat(60));
|
||||
console.log('\n🔌 Testing recovery from connection drops...');
|
||||
|
||||
let connectionCount = 0;
|
||||
let dropConnections = false;
|
||||
|
||||
// Create test server that can simulate connection drops
|
||||
const server = net.createServer(socket => {
|
||||
connectionCount++;
|
||||
console.log(` [Server] Connection ${connectionCount} established`);
|
||||
|
||||
if (dropConnections && connectionCount > 2) {
|
||||
console.log(` [Server] Simulating connection drop for connection ${connectionCount}`);
|
||||
setTimeout(() => {
|
||||
socket.destroy();
|
||||
}, 100);
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating SMTP client with connection recovery settings...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 2,
|
||||
maxMessages: 50,
|
||||
connectionTimeout: 2000
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 8; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@crashtest.example',
|
||||
to: [`recipient${i}@crashtest.example`],
|
||||
subject: `Connection Recovery Test ${i + 1}`,
|
||||
text: `Testing connection recovery, email ${i + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Phase 1: Sending initial emails (connections should succeed)...');
|
||||
const results1 = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
try {
|
||||
await smtpClient.sendMail(emails[i]);
|
||||
results1.push({ success: true, index: i });
|
||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
||||
} catch (error) {
|
||||
results1.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 2: Enabling connection drops...');
|
||||
dropConnections = true;
|
||||
|
||||
console.log(' Sending emails during connection instability...');
|
||||
const results2 = [];
|
||||
const promises = emails.slice(3).map((email, index) => {
|
||||
const actualIndex = index + 3;
|
||||
return smtpClient.sendMail(email).then(result => {
|
||||
console.log(` ✓ Email ${actualIndex + 1} recovered and sent`);
|
||||
return { success: true, index: actualIndex, result };
|
||||
}).catch(error => {
|
||||
console.log(` ✗ Email ${actualIndex + 1} failed permanently: ${error.message}`);
|
||||
return { success: false, index: actualIndex, error };
|
||||
});
|
||||
});
|
||||
|
||||
const results2Resolved = await Promise.all(promises);
|
||||
results2.push(...results2Resolved);
|
||||
|
||||
const totalSuccessful = [...results1, ...results2].filter(r => r.success).length;
|
||||
const totalFailed = [...results1, ...results2].filter(r => !r.success).length;
|
||||
|
||||
console.log(` Connection attempts: ${connectionCount}`);
|
||||
console.log(` Emails sent successfully: ${totalSuccessful}/${emails.length}`);
|
||||
console.log(` Failed emails: ${totalFailed}`);
|
||||
console.log(` Recovery effectiveness: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
expect(totalSuccessful).toBeGreaterThanOrEqual(3); // At least initial emails should succeed
|
||||
expect(connectionCount).toBeGreaterThanOrEqual(2); // Should have made multiple connection attempts
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-04: Recovery from Server Restart', async () => {
|
||||
console.log('\n💀 Testing recovery from server restart...');
|
||||
|
||||
// Start first server instance
|
||||
let server1 = net.createServer(socket => {
|
||||
console.log(' [Server1] Connection established');
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server1.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server1.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating client...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 1,
|
||||
connectionTimeout: 3000
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@serverrestart.test',
|
||||
to: [`recipient${i}@serverrestart.test`],
|
||||
subject: `Server Restart Recovery ${i + 1}`,
|
||||
text: `Testing server restart recovery, email ${i + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Sending first batch of emails...');
|
||||
await smtpClient.sendMail(emails[0]);
|
||||
console.log(' ✓ Email 1 sent successfully');
|
||||
|
||||
await smtpClient.sendMail(emails[1]);
|
||||
console.log(' ✓ Email 2 sent successfully');
|
||||
|
||||
console.log(' Simulating server restart by closing server...');
|
||||
server1.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 500));
|
||||
|
||||
console.log(' Starting new server instance on same port...');
|
||||
const server2 = net.createServer(socket => {
|
||||
console.log(' [Server2] Connection established after restart');
|
||||
socket.write('220 localhost SMTP Test Server Restarted\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server2.listen(port, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
console.log(' Sending emails after server restart...');
|
||||
const recoveryResults = [];
|
||||
|
||||
for (let i = 2; i < emails.length; i++) {
|
||||
try {
|
||||
await smtpClient.sendMail(emails[i]);
|
||||
recoveryResults.push({ success: true, index: i });
|
||||
console.log(` ✓ Email ${i + 1} sent after server recovery`);
|
||||
} catch (error) {
|
||||
recoveryResults.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const successfulRecovery = recoveryResults.filter(r => r.success).length;
|
||||
const totalSuccessful = 2 + successfulRecovery; // 2 from before restart + recovery
|
||||
|
||||
console.log(` Pre-restart emails: 2/2 successful`);
|
||||
console.log(` Post-restart emails: ${successfulRecovery}/${recoveryResults.length} successful`);
|
||||
console.log(` Overall success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
||||
console.log(` Server restart recovery: ${successfulRecovery > 0 ? 'Successful' : 'Failed'}`);
|
||||
|
||||
expect(successfulRecovery).toBeGreaterThanOrEqual(1); // At least some emails should work after restart
|
||||
|
||||
smtpClient.close();
|
||||
server2.close();
|
||||
} finally {
|
||||
// Ensure cleanup
|
||||
try {
|
||||
server1.close();
|
||||
} catch (e) { /* Already closed */ }
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-04: Error Recovery and State Management', async () => {
|
||||
console.log('\n⚠️ Testing error recovery and state management...');
|
||||
|
||||
let errorInjectionEnabled = false;
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (errorInjectionEnabled && line.startsWith('MAIL FROM')) {
|
||||
console.log(' [Server] Injecting error response');
|
||||
socket.write('550 Simulated server error\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
} else if (line === 'RSET') {
|
||||
socket.write('250 OK\r\n');
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating client with error handling...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 1,
|
||||
connectionTimeout: 3000
|
||||
});
|
||||
|
||||
const emails = [];
|
||||
for (let i = 0; i < 6; i++) {
|
||||
emails.push(new Email({
|
||||
from: 'sender@exception.test',
|
||||
to: [`recipient${i}@exception.test`],
|
||||
subject: `Error Recovery Test ${i + 1}`,
|
||||
text: `Testing error recovery, email ${i + 1}`
|
||||
}));
|
||||
}
|
||||
|
||||
console.log(' Phase 1: Sending emails normally...');
|
||||
await smtpClient.sendMail(emails[0]);
|
||||
console.log(' ✓ Email 1 sent successfully');
|
||||
|
||||
await smtpClient.sendMail(emails[1]);
|
||||
console.log(' ✓ Email 2 sent successfully');
|
||||
|
||||
console.log(' Phase 2: Enabling error injection...');
|
||||
errorInjectionEnabled = true;
|
||||
|
||||
console.log(' Sending emails with error injection...');
|
||||
const recoveryResults = [];
|
||||
|
||||
for (let i = 2; i < 4; i++) {
|
||||
try {
|
||||
await smtpClient.sendMail(emails[i]);
|
||||
recoveryResults.push({ success: true, index: i });
|
||||
console.log(` ✓ Email ${i + 1} sent despite errors`);
|
||||
} catch (error) {
|
||||
recoveryResults.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(' Phase 3: Disabling error injection...');
|
||||
errorInjectionEnabled = false;
|
||||
|
||||
console.log(' Sending final emails (recovery validation)...');
|
||||
for (let i = 4; i < emails.length; i++) {
|
||||
try {
|
||||
await smtpClient.sendMail(emails[i]);
|
||||
recoveryResults.push({ success: true, index: i });
|
||||
console.log(` ✓ Email ${i + 1} sent after recovery`);
|
||||
} catch (error) {
|
||||
recoveryResults.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
const successful = recoveryResults.filter(r => r.success).length;
|
||||
const totalSuccessful = 2 + successful; // 2 initial + recovery phase
|
||||
|
||||
console.log(` Pre-error emails: 2/2 successful`);
|
||||
console.log(` Error/recovery phase emails: ${successful}/${recoveryResults.length} successful`);
|
||||
console.log(` Total success rate: ${((totalSuccessful / emails.length) * 100).toFixed(1)}%`);
|
||||
console.log(` Error recovery: ${successful >= recoveryResults.length - 2 ? 'Effective' : 'Partial'}`);
|
||||
|
||||
expect(totalSuccessful).toBeGreaterThanOrEqual(4); // At least initial + some recovery
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-04: Resource Management During Issues', async () => {
|
||||
console.log('\n🧠 Testing resource management during connection issues...');
|
||||
|
||||
let memoryBefore = process.memoryUsage();
|
||||
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating client for resource management test...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100
|
||||
});
|
||||
|
||||
console.log(' Creating emails with various content types...');
|
||||
const emails = [
|
||||
new Email({
|
||||
from: 'sender@resource.test',
|
||||
to: ['recipient1@resource.test'],
|
||||
subject: 'Resource Test - Normal',
|
||||
text: 'Normal email content'
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@resource.test',
|
||||
to: ['recipient2@resource.test'],
|
||||
subject: 'Resource Test - Large Content',
|
||||
text: 'X'.repeat(50000) // Large content
|
||||
}),
|
||||
new Email({
|
||||
from: 'sender@resource.test',
|
||||
to: ['recipient3@resource.test'],
|
||||
subject: 'Resource Test - Unicode',
|
||||
text: '🎭🎪🎨🎯🎲🎸🎺🎻🎼🎵🎶🎷'.repeat(100)
|
||||
})
|
||||
];
|
||||
|
||||
console.log(' Sending emails and monitoring resource usage...');
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < emails.length; i++) {
|
||||
console.log(` Testing email ${i + 1} (${emails[i].subject.split(' - ')[1]})...`);
|
||||
|
||||
try {
|
||||
// Monitor memory usage before sending
|
||||
const memBefore = process.memoryUsage();
|
||||
console.log(` Memory before: ${Math.round(memBefore.heapUsed / 1024 / 1024)}MB`);
|
||||
|
||||
await smtpClient.sendMail(emails[i]);
|
||||
|
||||
const memAfter = process.memoryUsage();
|
||||
console.log(` Memory after: ${Math.round(memAfter.heapUsed / 1024 / 1024)}MB`);
|
||||
|
||||
const memIncrease = memAfter.heapUsed - memBefore.heapUsed;
|
||||
console.log(` Memory increase: ${Math.round(memIncrease / 1024)}KB`);
|
||||
|
||||
results.push({
|
||||
success: true,
|
||||
index: i,
|
||||
memoryIncrease: memIncrease
|
||||
});
|
||||
console.log(` ✓ Email ${i + 1} sent successfully`);
|
||||
|
||||
} catch (error) {
|
||||
results.push({ success: false, index: i, error });
|
||||
console.log(` ✗ Email ${i + 1} failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Force garbage collection if available
|
||||
if (global.gc) {
|
||||
global.gc();
|
||||
}
|
||||
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
const successful = results.filter(r => r.success).length;
|
||||
const totalMemoryIncrease = results.reduce((sum, r) => sum + (r.memoryIncrease || 0), 0);
|
||||
|
||||
console.log(` Resource management: ${successful}/${emails.length} emails processed`);
|
||||
console.log(` Total memory increase: ${Math.round(totalMemoryIncrease / 1024)}KB`);
|
||||
console.log(` Resource efficiency: ${((successful / emails.length) * 100).toFixed(1)}%`);
|
||||
|
||||
expect(successful).toBeGreaterThanOrEqual(2); // Most emails should succeed
|
||||
expect(totalMemoryIncrease).toBeLessThan(100 * 1024 * 1024); // Less than 100MB increase
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-04: Test Summary', async () => {
|
||||
console.log('\n✅ CREL-04: Crash Recovery Reliability Tests completed');
|
||||
console.log('💥 All connection recovery scenarios tested successfully');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
503
test/suite/smtpclient_reliability/test.crel-05.memory-leaks.ts
Normal file
503
test/suite/smtpclient_reliability/test.crel-05.memory-leaks.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { createTestSmtpClient } from '../../helpers/smtp.client.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
|
||||
// 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
|
||||
}
|
||||
};
|
||||
|
||||
tap.test('CREL-05: Connection Pool Memory Management', async () => {
|
||||
console.log('\n🧠 Testing SMTP Client Memory Leak Prevention');
|
||||
console.log('=' .repeat(60));
|
||||
console.log('\n🏊 Testing connection pool memory management...');
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
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 = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
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}`
|
||||
}));
|
||||
}
|
||||
|
||||
// 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 < 3.0 ? 'Good (< 3MB growth)' : 'Potential leak detected'}`);
|
||||
|
||||
expect(leakGrowth).toBeLessThan(5.0); // Allow some memory growth but detect major leaks
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-05: Email Object Memory Lifecycle', async () => {
|
||||
console.log('\n📧 Testing email object memory lifecycle...');
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
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>`
|
||||
}));
|
||||
}
|
||||
|
||||
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'}`);
|
||||
|
||||
expect(maxMemoryIncrease).toBeLessThan(15); // Allow reasonable memory usage
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-05: Long-Running Client Memory Stability', async () => {
|
||||
console.log('\n⏱️ Testing long-running client memory stability...');
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
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 = 100; // Reduced for test efficiency
|
||||
const measurementInterval = 20; // Measure every 20 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}`
|
||||
});
|
||||
|
||||
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 < 10 ? 'Excellent' : growthRate < 25 ? 'Good' : 'Concerning'}`);
|
||||
|
||||
expect(growthRate).toBeLessThan(50); // Allow reasonable growth but detect major leaks
|
||||
}
|
||||
|
||||
expect(emailsSent).toBeGreaterThanOrEqual(totalEmails - 5); // Most emails should succeed
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-05: Large Content Memory Management', async () => {
|
||||
console.log('\n🌊 Testing large content memory management...');
|
||||
|
||||
// Create test server
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 1
|
||||
});
|
||||
|
||||
const initialMemory = getMemoryUsage();
|
||||
console.log(` Initial memory: ${initialMemory.heapUsed}MB heap`);
|
||||
|
||||
console.log(' Testing with various content sizes...');
|
||||
const contentSizes = [
|
||||
{ size: 1024, name: '1KB' },
|
||||
{ size: 10240, name: '10KB' },
|
||||
{ size: 102400, name: '100KB' },
|
||||
{ size: 256000, name: '250KB' }
|
||||
];
|
||||
|
||||
for (const contentTest of contentSizes) {
|
||||
console.log(` Testing ${contentTest.name} content size...`);
|
||||
|
||||
const beforeMemory = getMemoryUsage();
|
||||
|
||||
// Create large text content
|
||||
const largeText = 'X'.repeat(contentTest.size);
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@largemem.test',
|
||||
to: ['recipient@largemem.test'],
|
||||
subject: `Large Content Test - ${contentTest.name}`,
|
||||
text: largeText
|
||||
});
|
||||
|
||||
try {
|
||||
await smtpClient.sendMail(email);
|
||||
console.log(` ✓ ${contentTest.name} email sent successfully`);
|
||||
} catch (error) {
|
||||
console.log(` ✗ ${contentTest.name} email failed: ${error.message}`);
|
||||
}
|
||||
|
||||
// Force cleanup
|
||||
forceGC();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
const afterMemory = getMemoryUsage();
|
||||
const memoryDiff = afterMemory.heapUsed - beforeMemory.heapUsed;
|
||||
|
||||
console.log(` Memory impact: ${memoryDiff > 0 ? '+' : ''}${memoryDiff.toFixed(2)}MB`);
|
||||
console.log(` Efficiency: ${Math.abs(memoryDiff) < (contentTest.size / 1024 / 1024) * 2 ? 'Good' : 'High memory usage'}`);
|
||||
}
|
||||
|
||||
const finalMemory = getMemoryUsage();
|
||||
const totalMemoryIncrease = finalMemory.heapUsed - initialMemory.heapUsed;
|
||||
|
||||
console.log(`\n Large content memory summary:`);
|
||||
console.log(` Total memory increase: +${totalMemoryIncrease.toFixed(2)}MB`);
|
||||
console.log(` Memory management efficiency: ${totalMemoryIncrease < 5 ? 'Excellent' : 'Needs optimization'}`);
|
||||
|
||||
expect(totalMemoryIncrease).toBeLessThan(20); // Allow reasonable memory usage for large content
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-05: Test Summary', async () => {
|
||||
console.log('\n✅ CREL-05: Memory Leak Prevention Reliability Tests completed');
|
||||
console.log('🧠 All memory management scenarios tested successfully');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
@@ -0,0 +1,558 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as net from 'net';
|
||||
import { createTestSmtpClient } from '../../helpers/smtp.client.ts';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.ts';
|
||||
|
||||
tap.test('CREL-06: Simultaneous Connection Management', async () => {
|
||||
console.log('\n⚡ Testing SMTP Client Concurrent Operation Safety');
|
||||
console.log('=' .repeat(60));
|
||||
console.log('\n🔗 Testing simultaneous connection management safety...');
|
||||
|
||||
let connectionCount = 0;
|
||||
let activeConnections = 0;
|
||||
const connectionLog: string[] = [];
|
||||
|
||||
// Create test server that tracks connections
|
||||
const server = net.createServer(socket => {
|
||||
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})`);
|
||||
});
|
||||
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating multiple SMTP clients with shared connection pool settings...');
|
||||
const clients = [];
|
||||
|
||||
for (let i = 0; i < 5; i++) {
|
||||
clients.push(createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 3, // Allow up to 3 connections
|
||||
maxMessages: 10,
|
||||
connectionTimeout: 2000
|
||||
}));
|
||||
}
|
||||
|
||||
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}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
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'}`);
|
||||
|
||||
expect(successful).toBeGreaterThanOrEqual(totalEmails - 5); // Allow some failures
|
||||
expect(activeConnections).toEqual(0); // All connections should be closed
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-06: Concurrent Queue Operations', async () => {
|
||||
console.log('\n🔒 Testing concurrent queue operations...');
|
||||
|
||||
let messageProcessingOrder: string[] = [];
|
||||
|
||||
// Create test server that tracks message processing order
|
||||
const server = net.createServer(socket => {
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
let inData = false;
|
||||
let currentData = '';
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (inData) {
|
||||
if (line === '.') {
|
||||
// Extract Message-ID from email data
|
||||
const messageIdMatch = currentData.match(/Message-ID:\s*<([^>]+)>/);
|
||||
if (messageIdMatch) {
|
||||
messageProcessingOrder.push(messageIdMatch[1]);
|
||||
console.log(` [Server] Processing: ${messageIdMatch[1]}`);
|
||||
}
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
inData = false;
|
||||
currentData = '';
|
||||
} else {
|
||||
currentData += line + '\r\n';
|
||||
}
|
||||
} else {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
inData = true;
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating SMTP client for concurrent queue operations...');
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 2,
|
||||
maxMessages: 50
|
||||
});
|
||||
|
||||
console.log(' Launching concurrent queue operations...');
|
||||
const operations: Promise<any>[] = [];
|
||||
const emailGroups = ['A', 'B', 'C', 'D'];
|
||||
|
||||
// Create concurrent operations that use 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 safety for group ${group}, email ${i + 1}`
|
||||
});
|
||||
|
||||
const operation = smtpClient.sendMail(email).then(result => {
|
||||
return {
|
||||
success: true,
|
||||
group,
|
||||
index: i,
|
||||
messageId: result.messageId,
|
||||
timestamp: Date.now()
|
||||
};
|
||||
}).catch(error => {
|
||||
return {
|
||||
success: false,
|
||||
group,
|
||||
index: i,
|
||||
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
|
||||
const groupCounts = emailGroups.reduce((acc, group) => {
|
||||
acc[group] = messageProcessingOrder.filter(id => 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' : 'Some messages lost'}`);
|
||||
|
||||
expect(successful).toBeGreaterThanOrEqual(operations.length - 2); // Allow minimal failures
|
||||
|
||||
smtpClient.close();
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-06: Concurrent Error Handling', async () => {
|
||||
console.log('\n❌ Testing concurrent error handling safety...');
|
||||
|
||||
let errorInjectionPhase = false;
|
||||
let connectionAttempts = 0;
|
||||
|
||||
// Create test server that can inject errors
|
||||
const server = net.createServer(socket => {
|
||||
connectionAttempts++;
|
||||
console.log(` [Server] Connection attempt ${connectionAttempts}`);
|
||||
|
||||
if (errorInjectionPhase && Math.random() < 0.4) {
|
||||
console.log(` [Server] Injecting connection error ${connectionAttempts}`);
|
||||
socket.destroy();
|
||||
return;
|
||||
}
|
||||
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
socket.on('data', (data) => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (errorInjectionPhase && line.startsWith('MAIL FROM') && Math.random() < 0.3) {
|
||||
console.log(' [Server] Injecting SMTP error');
|
||||
socket.write('450 Temporary failure, please retry\r\n');
|
||||
return;
|
||||
}
|
||||
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating multiple clients for concurrent error testing...');
|
||||
const clients = [];
|
||||
|
||||
for (let i = 0; i < 4; i++) {
|
||||
clients.push(createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 2,
|
||||
connectionTimeout: 3000
|
||||
}));
|
||||
}
|
||||
|
||||
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}`
|
||||
}),
|
||||
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' : 'Some failures'}`);
|
||||
|
||||
expect(phase1Success).toBeGreaterThanOrEqual(phase1Results.length - 1); // Most should succeed
|
||||
expect(phase2Success).toBeGreaterThanOrEqual(1); // Some should succeed despite errors
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-06: Resource Contention Management', async () => {
|
||||
console.log('\n🏁 Testing resource contention management...');
|
||||
|
||||
// Create test server with limited capacity
|
||||
const server = net.createServer(socket => {
|
||||
console.log(' [Server] New connection established');
|
||||
|
||||
socket.write('220 localhost SMTP Test Server\r\n');
|
||||
|
||||
// Add some delay to simulate slow server
|
||||
socket.on('data', (data) => {
|
||||
setTimeout(() => {
|
||||
const lines = data.toString().split('\r\n');
|
||||
|
||||
lines.forEach(line => {
|
||||
if (line.startsWith('EHLO') || line.startsWith('HELO')) {
|
||||
socket.write('250-localhost\r\n');
|
||||
socket.write('250 SIZE 10485760\r\n');
|
||||
} else if (line.startsWith('MAIL FROM:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line.startsWith('RCPT TO:')) {
|
||||
socket.write('250 OK\r\n');
|
||||
} else if (line === 'DATA') {
|
||||
socket.write('354 Send data\r\n');
|
||||
} else if (line === '.') {
|
||||
socket.write('250 OK Message accepted\r\n');
|
||||
} else if (line === 'QUIT') {
|
||||
socket.write('221 Bye\r\n');
|
||||
socket.end();
|
||||
}
|
||||
});
|
||||
}, 20); // Add 20ms delay to responses
|
||||
});
|
||||
});
|
||||
|
||||
server.maxConnections = 3; // Limit server connections
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
server.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const port = (server.address() as net.AddressInfo).port;
|
||||
|
||||
try {
|
||||
console.log(' Creating 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(createTestSmtpClient({
|
||||
host: '127.0.0.1',
|
||||
port: port,
|
||||
secure: false,
|
||||
maxConnections: 1, // Force contention
|
||||
maxMessages: 10,
|
||||
connectionTimeout: 3000
|
||||
}));
|
||||
}
|
||||
|
||||
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}`
|
||||
}),
|
||||
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'}`);
|
||||
|
||||
expect(successful).toBeGreaterThanOrEqual(emails.length * 0.7); // At least 70% should succeed
|
||||
|
||||
} finally {
|
||||
server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CREL-06: Test Summary', async () => {
|
||||
console.log('\n✅ CREL-06: Concurrent Operation Safety Reliability Tests completed');
|
||||
console.log('⚡ All concurrency safety scenarios tested successfully');
|
||||
});
|
||||
|
||||
tap.start();
|
||||
@@ -0,0 +1,52 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { createTestServer } from '../../helpers/server.loader.ts';
|
||||
import { createTestSmtpClient } from '../../helpers/smtp.client.ts';
|
||||
|
||||
tap.test('CREL-07: Resource Cleanup Tests', async () => {
|
||||
console.log('\n🧹 Testing SMTP Client Resource Cleanup');
|
||||
console.log('=' .repeat(60));
|
||||
|
||||
const testServer = await createTestServer({});
|
||||
|
||||
try {
|
||||
console.log('\nTest 1: Basic client creation and cleanup');
|
||||
|
||||
// Create a client
|
||||
const smtpClient = createTestSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port
|
||||
});
|
||||
console.log(' ✓ Client created');
|
||||
|
||||
// Verify connection
|
||||
try {
|
||||
const verifyResult = await smtpClient.verify();
|
||||
console.log(' ✓ Connection verified:', verifyResult);
|
||||
} catch (error) {
|
||||
console.log(' ⚠️ Verify failed:', error.message);
|
||||
}
|
||||
|
||||
// Close the client
|
||||
smtpClient.close();
|
||||
console.log(' ✓ Client closed');
|
||||
|
||||
console.log('\nTest 2: Multiple close calls');
|
||||
const testClient = createTestSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port
|
||||
});
|
||||
|
||||
// Close multiple times - should not throw
|
||||
testClient.close();
|
||||
testClient.close();
|
||||
testClient.close();
|
||||
console.log(' ✓ Multiple close calls handled safely');
|
||||
|
||||
console.log('\n✅ CREL-07: Resource cleanup tests completed');
|
||||
|
||||
} finally {
|
||||
testServer.server.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.start();
|
||||
Reference in New Issue
Block a user