558 lines
20 KiB
TypeScript
558 lines
20 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as net from 'net';
|
|
import { createTestSmtpClient } from '../../helpers/smtp.client.js';
|
|
import { Email } from '../../../ts/mail/core/classes.email.js';
|
|
|
|
tap.test('CREL-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(); |