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();
|
||||
Reference in New Issue
Block a user