update
This commit is contained in:
@ -0,0 +1,136 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server for basic connection test', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2525,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2525);
|
||||
});
|
||||
|
||||
tap.test('CCM-01: Basic TCP Connection - should connect to SMTP server', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Create SMTP client
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Verify connection
|
||||
const isConnected = await smtpClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ Basic TCP connection established in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`❌ Basic TCP connection failed after ${duration}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-01: Basic TCP Connection - should report connection status', async () => {
|
||||
expect(smtpClient.isConnected()).toBeTrue();
|
||||
|
||||
const poolStatus = smtpClient.getPoolStatus();
|
||||
console.log('📊 Connection pool status:', poolStatus);
|
||||
|
||||
// For non-pooled connection, should have 1 connection
|
||||
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
|
||||
expect(poolStatus.active).toBeGreaterThanOrEqual(0);
|
||||
});
|
||||
|
||||
tap.test('CCM-01: Basic TCP Connection - should handle multiple connect/disconnect cycles', async () => {
|
||||
// Close existing connection
|
||||
await smtpClient.close();
|
||||
expect(smtpClient.isConnected()).toBeFalse();
|
||||
|
||||
// Create new client and test reconnection
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const cycleClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
const isConnected = await cycleClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
await cycleClient.close();
|
||||
expect(cycleClient.isConnected()).toBeFalse();
|
||||
|
||||
console.log(`✅ Connection cycle ${i + 1} completed`);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-01: Basic TCP Connection - should fail with invalid host', async () => {
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
const invalidClient = createSmtpClient({
|
||||
host: 'invalid.host.that.does.not.exist',
|
||||
port: 2525,
|
||||
secure: false,
|
||||
connectionTimeout: 3000
|
||||
});
|
||||
|
||||
await invalidClient.verify();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Correctly failed to connect to invalid host');
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CCM-01: Basic TCP Connection - should timeout on unresponsive port', async () => {
|
||||
let errorThrown = false;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const timeoutClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: 9999, // Port that's not listening
|
||||
secure: false,
|
||||
connectionTimeout: 2000
|
||||
});
|
||||
|
||||
await timeoutClient.verify();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
const duration = Date.now() - startTime;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(duration).toBeLessThan(3000); // Should timeout within 3 seconds
|
||||
console.log(`✅ Connection timeout working correctly (${duration}ms)`);
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('cleanup - close SMTP client', async () => {
|
||||
if (smtpClient && smtpClient.isConnected()) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
162
test/suite/smtpclient_connection/test.ccm-02.tls-connection.ts
Normal file
162
test/suite/smtpclient_connection/test.ccm-02.tls-connection.ts
Normal file
@ -0,0 +1,162 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server with TLS', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2526,
|
||||
tlsEnabled: true,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2526);
|
||||
expect(testServer.config.tlsEnabled).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CCM-02: TLS Connection - should establish secure connection', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Create SMTP client with TLS
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
connectionTimeout: 10000,
|
||||
tls: {
|
||||
rejectUnauthorized: false // For self-signed test certificates
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Verify secure connection
|
||||
const isConnected = await smtpClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ TLS connection established in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`❌ TLS connection failed after ${duration}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-02: TLS Connection - should send email over secure connection', async () => {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'TLS Connection Test',
|
||||
text: 'This email was sent over a secure TLS connection',
|
||||
html: '<p>This email was sent over a <strong>secure TLS connection</strong></p>'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
expect(result.acceptedRecipients).toContain('recipient@example.com');
|
||||
expect(result.rejectedRecipients).toBeArray();
|
||||
expect(result.rejectedRecipients.length).toEqual(0);
|
||||
|
||||
console.log('✅ Email sent successfully over TLS');
|
||||
console.log('📧 Message ID:', result.messageId);
|
||||
});
|
||||
|
||||
tap.test('CCM-02: TLS Connection - should validate certificate options', async () => {
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
// Create client with strict certificate validation
|
||||
const strictClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
connectionTimeout: 5000,
|
||||
tls: {
|
||||
rejectUnauthorized: true, // Strict validation
|
||||
servername: testServer.hostname
|
||||
}
|
||||
});
|
||||
|
||||
await strictClient.verify();
|
||||
await strictClient.close();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
// Expected to fail with self-signed certificate
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ Certificate validation working correctly');
|
||||
}
|
||||
|
||||
// For self-signed certs, strict validation should fail
|
||||
expect(errorThrown).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CCM-02: TLS Connection - should support custom TLS options', async () => {
|
||||
const tlsClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: true,
|
||||
connectionTimeout: 10000,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
minVersion: 'TLSv1.2',
|
||||
maxVersion: 'TLSv1.3'
|
||||
}
|
||||
});
|
||||
|
||||
const isConnected = await tlsClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
await tlsClient.close();
|
||||
console.log('✅ Custom TLS options accepted');
|
||||
});
|
||||
|
||||
tap.test('CCM-02: TLS Connection - should handle TLS handshake errors', async () => {
|
||||
// Start a non-TLS server to test handshake failure
|
||||
const nonTlsServer = await startTestServer({
|
||||
port: 2527,
|
||||
tlsEnabled: false
|
||||
});
|
||||
|
||||
let errorThrown = false;
|
||||
|
||||
try {
|
||||
const failClient = createSmtpClient({
|
||||
host: nonTlsServer.hostname,
|
||||
port: nonTlsServer.port,
|
||||
secure: true, // Try TLS on non-TLS server
|
||||
connectionTimeout: 5000,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
await failClient.verify();
|
||||
} catch (error) {
|
||||
errorThrown = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ TLS handshake error handled correctly');
|
||||
}
|
||||
|
||||
expect(errorThrown).toBeTrue();
|
||||
|
||||
await stopTestServer(nonTlsServer);
|
||||
});
|
||||
|
||||
tap.test('cleanup - close SMTP client', async () => {
|
||||
if (smtpClient && smtpClient.isConnected()) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
200
test/suite/smtpclient_connection/test.ccm-03.starttls-upgrade.ts
Normal file
200
test/suite/smtpclient_connection/test.ccm-03.starttls-upgrade.ts
Normal file
@ -0,0 +1,200 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server with STARTTLS support', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2528,
|
||||
tlsEnabled: true, // Enables STARTTLS capability
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2528);
|
||||
});
|
||||
|
||||
tap.test('CCM-03: STARTTLS Upgrade - should upgrade plain connection to TLS', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Create SMTP client starting with plain connection
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false, // Start with plain connection
|
||||
connectionTimeout: 10000,
|
||||
tls: {
|
||||
rejectUnauthorized: false // For self-signed test certificates
|
||||
},
|
||||
debug: true
|
||||
});
|
||||
|
||||
// The client should automatically upgrade to TLS via STARTTLS
|
||||
const isConnected = await smtpClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ STARTTLS upgrade completed in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`❌ STARTTLS upgrade failed after ${duration}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-03: STARTTLS Upgrade - should send email after upgrade', async () => {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'STARTTLS Upgrade Test',
|
||||
text: 'This email was sent after STARTTLS upgrade',
|
||||
html: '<p>This email was sent after <strong>STARTTLS upgrade</strong></p>'
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
expect(result.acceptedRecipients).toContain('recipient@example.com');
|
||||
expect(result.rejectedRecipients.length).toEqual(0);
|
||||
|
||||
console.log('✅ Email sent successfully after STARTTLS upgrade');
|
||||
console.log('📧 Message ID:', result.messageId);
|
||||
});
|
||||
|
||||
tap.test('CCM-03: STARTTLS Upgrade - should handle servers without STARTTLS', async () => {
|
||||
// Start a server without TLS support
|
||||
const plainServer = await startTestServer({
|
||||
port: 2529,
|
||||
tlsEnabled: false // No STARTTLS support
|
||||
});
|
||||
|
||||
try {
|
||||
const plainClient = createSmtpClient({
|
||||
host: plainServer.hostname,
|
||||
port: plainServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Should still connect but without TLS
|
||||
const isConnected = await plainClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
// Send test email over plain connection
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Plain Connection Test',
|
||||
text: 'This email was sent over plain connection'
|
||||
});
|
||||
|
||||
const result = await plainClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
await plainClient.close();
|
||||
console.log('✅ Successfully handled server without STARTTLS');
|
||||
|
||||
} finally {
|
||||
await stopTestServer(plainServer);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-03: STARTTLS Upgrade - should respect TLS options during upgrade', async () => {
|
||||
const customTlsClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false, // Start plain
|
||||
connectionTimeout: 10000,
|
||||
tls: {
|
||||
rejectUnauthorized: false,
|
||||
minVersion: 'TLSv1.2',
|
||||
ciphers: 'HIGH:!aNULL:!MD5'
|
||||
}
|
||||
});
|
||||
|
||||
const isConnected = await customTlsClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
await customTlsClient.close();
|
||||
console.log('✅ Custom TLS options applied during STARTTLS upgrade');
|
||||
});
|
||||
|
||||
tap.test('CCM-03: STARTTLS Upgrade - should handle upgrade failures gracefully', async () => {
|
||||
// Create a mock scenario where STARTTLS might fail
|
||||
// This would typically happen with certificate issues or protocol mismatches
|
||||
|
||||
let errorCaught = false;
|
||||
|
||||
try {
|
||||
const strictTlsClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
tls: {
|
||||
rejectUnauthorized: true, // Strict validation with self-signed cert
|
||||
servername: 'wrong.hostname.com' // Wrong hostname
|
||||
}
|
||||
});
|
||||
|
||||
await strictTlsClient.verify();
|
||||
await strictTlsClient.close();
|
||||
} catch (error) {
|
||||
errorCaught = true;
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
console.log('✅ STARTTLS upgrade failure handled gracefully');
|
||||
}
|
||||
|
||||
// Should fail due to certificate validation
|
||||
expect(errorCaught).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CCM-03: STARTTLS Upgrade - should maintain connection state after upgrade', async () => {
|
||||
const stateClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 10000,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
// Connect and verify
|
||||
await stateClient.verify();
|
||||
expect(stateClient.isConnected()).toBeTrue();
|
||||
|
||||
// Send multiple emails to verify connection remains stable
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `STARTTLS State Test ${i + 1}`,
|
||||
text: `Message ${i + 1} after STARTTLS upgrade`
|
||||
});
|
||||
|
||||
const result = await stateClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
}
|
||||
|
||||
await stateClient.close();
|
||||
console.log('✅ Connection state maintained after STARTTLS upgrade');
|
||||
});
|
||||
|
||||
tap.test('cleanup - close SMTP client', async () => {
|
||||
if (smtpClient && smtpClient.isConnected()) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -0,0 +1,238 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let pooledClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server for pooling test', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2530,
|
||||
tlsEnabled: false,
|
||||
authRequired: false,
|
||||
maxConnections: 10
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2530);
|
||||
});
|
||||
|
||||
tap.test('CCM-04: Connection Pooling - should create pooled client', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
// Create pooled SMTP client
|
||||
pooledClient = createPooledSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxConnections: 5,
|
||||
maxMessages: 100,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Verify connection pool is working
|
||||
const isConnected = await pooledClient.verify();
|
||||
expect(isConnected).toBeTrue();
|
||||
|
||||
const poolStatus = pooledClient.getPoolStatus();
|
||||
console.log('📊 Initial pool status:', poolStatus);
|
||||
expect(poolStatus.total).toBeGreaterThanOrEqual(0);
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ Connection pool created in ${duration}ms`);
|
||||
|
||||
} catch (error) {
|
||||
const duration = Date.now() - startTime;
|
||||
console.error(`❌ Connection pool creation failed after ${duration}ms:`, error);
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-04: Connection Pooling - should handle concurrent connections', async () => {
|
||||
// Send multiple emails concurrently
|
||||
const emailPromises = [];
|
||||
const concurrentCount = 5;
|
||||
|
||||
for (let i = 0; i < concurrentCount; i++) {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: `recipient${i}@example.com`,
|
||||
subject: `Concurrent Email ${i}`,
|
||||
text: `This is concurrent email number ${i}`
|
||||
});
|
||||
|
||||
emailPromises.push(pooledClient.sendMail(email));
|
||||
}
|
||||
|
||||
// Wait for all emails to be sent
|
||||
const results = await Promise.all(emailPromises);
|
||||
|
||||
// Check all were successful
|
||||
results.forEach((result, index) => {
|
||||
expect(result.success).toBeTrue();
|
||||
expect(result.acceptedRecipients).toContain(`recipient${index}@example.com`);
|
||||
});
|
||||
|
||||
// Check pool status after concurrent sends
|
||||
const poolStatus = pooledClient.getPoolStatus();
|
||||
console.log('📊 Pool status after concurrent sends:', poolStatus);
|
||||
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
|
||||
expect(poolStatus.total).toBeLessThanOrEqual(5); // Should not exceed max
|
||||
|
||||
console.log(`✅ Successfully sent ${concurrentCount} concurrent emails`);
|
||||
});
|
||||
|
||||
tap.test('CCM-04: Connection Pooling - should reuse connections', async () => {
|
||||
// Get initial pool status
|
||||
const initialStatus = pooledClient.getPoolStatus();
|
||||
console.log('📊 Initial status:', initialStatus);
|
||||
|
||||
// Send emails sequentially to test connection reuse
|
||||
const emailCount = 10;
|
||||
const connectionCounts = [];
|
||||
|
||||
for (let i = 0; i < emailCount; i++) {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Sequential Email ${i}`,
|
||||
text: `Testing connection reuse - email ${i}`
|
||||
});
|
||||
|
||||
await pooledClient.sendMail(email);
|
||||
|
||||
const status = pooledClient.getPoolStatus();
|
||||
connectionCounts.push(status.total);
|
||||
}
|
||||
|
||||
// Check that connections were reused (total shouldn't grow linearly)
|
||||
const maxConnections = Math.max(...connectionCounts);
|
||||
expect(maxConnections).toBeLessThan(emailCount); // Should reuse connections
|
||||
|
||||
console.log(`✅ Sent ${emailCount} emails using max ${maxConnections} connections`);
|
||||
console.log('📊 Connection counts:', connectionCounts);
|
||||
});
|
||||
|
||||
tap.test('CCM-04: Connection Pooling - should respect max connections limit', async () => {
|
||||
// Create a client with small pool
|
||||
const limitedClient = createPooledSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxConnections: 2, // Very small pool
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send many concurrent emails
|
||||
const emailPromises = [];
|
||||
for (let i = 0; i < 10; i++) {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: `test${i}@example.com`,
|
||||
subject: `Pool Limit Test ${i}`,
|
||||
text: 'Testing pool limits'
|
||||
});
|
||||
emailPromises.push(limitedClient.sendMail(email));
|
||||
}
|
||||
|
||||
// Monitor pool during sending
|
||||
const checkInterval = setInterval(() => {
|
||||
const status = limitedClient.getPoolStatus();
|
||||
console.log('📊 Pool status during load:', status);
|
||||
expect(status.total).toBeLessThanOrEqual(2); // Should never exceed max
|
||||
}, 100);
|
||||
|
||||
await Promise.all(emailPromises);
|
||||
clearInterval(checkInterval);
|
||||
|
||||
await limitedClient.close();
|
||||
console.log('✅ Connection pool respected max connections limit');
|
||||
});
|
||||
|
||||
tap.test('CCM-04: Connection Pooling - should handle connection failures in pool', async () => {
|
||||
// Create a new pooled client
|
||||
const resilientClient = createPooledSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxConnections: 3,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send some emails successfully
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Pre-failure Email ${i}`,
|
||||
text: 'Before simulated failure'
|
||||
});
|
||||
|
||||
const result = await resilientClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
}
|
||||
|
||||
// Pool should recover and continue working
|
||||
const poolStatus = resilientClient.getPoolStatus();
|
||||
console.log('📊 Pool status after recovery test:', poolStatus);
|
||||
expect(poolStatus.total).toBeGreaterThanOrEqual(1);
|
||||
|
||||
await resilientClient.close();
|
||||
console.log('✅ Connection pool handled failures gracefully');
|
||||
});
|
||||
|
||||
tap.test('CCM-04: Connection Pooling - should clean up idle connections', async () => {
|
||||
// Create client with specific idle settings
|
||||
const idleClient = createPooledSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxConnections: 5,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send burst of emails
|
||||
const promises = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'test@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Idle Test ${i}`,
|
||||
text: 'Testing idle cleanup'
|
||||
});
|
||||
promises.push(idleClient.sendMail(email));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const activeStatus = idleClient.getPoolStatus();
|
||||
console.log('📊 Pool status after burst:', activeStatus);
|
||||
|
||||
// Wait for connections to become idle
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
const idleStatus = idleClient.getPoolStatus();
|
||||
console.log('📊 Pool status after idle period:', idleStatus);
|
||||
|
||||
await idleClient.close();
|
||||
console.log('✅ Idle connection management working');
|
||||
});
|
||||
|
||||
tap.test('cleanup - close pooled client', async () => {
|
||||
if (pooledClient && pooledClient.isConnected()) {
|
||||
await pooledClient.close();
|
||||
|
||||
// Verify pool is cleaned up
|
||||
const finalStatus = pooledClient.getPoolStatus();
|
||||
console.log('📊 Final pool status:', finalStatus);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
290
test/suite/smtpclient_connection/test.ccm-05.connection-reuse.ts
Normal file
290
test/suite/smtpclient_connection/test.ccm-05.connection-reuse.ts
Normal file
@ -0,0 +1,290 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
|
||||
let testServer: ITestServer;
|
||||
let smtpClient: SmtpClient;
|
||||
|
||||
tap.test('setup - start SMTP server for connection reuse test', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2531,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2531);
|
||||
});
|
||||
|
||||
tap.test('CCM-05: Connection Reuse - should reuse single connection for multiple emails', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Verify initial connection
|
||||
await smtpClient.verify();
|
||||
expect(smtpClient.isConnected()).toBeTrue();
|
||||
|
||||
// Send multiple emails on same connection
|
||||
const emailCount = 5;
|
||||
const results = [];
|
||||
|
||||
for (let i = 0; i < emailCount; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Connection Reuse Test ${i + 1}`,
|
||||
text: `This is email ${i + 1} using the same connection`
|
||||
});
|
||||
|
||||
const result = await smtpClient.sendMail(email);
|
||||
results.push(result);
|
||||
|
||||
// Connection should remain open
|
||||
expect(smtpClient.isConnected()).toBeTrue();
|
||||
}
|
||||
|
||||
// All emails should succeed
|
||||
results.forEach((result, index) => {
|
||||
expect(result.success).toBeTrue();
|
||||
console.log(`✅ Email ${index + 1} sent successfully`);
|
||||
});
|
||||
|
||||
const duration = Date.now() - startTime;
|
||||
console.log(`✅ Sent ${emailCount} emails on single connection in ${duration}ms`);
|
||||
});
|
||||
|
||||
tap.test('CCM-05: Connection Reuse - should track message count per connection', async () => {
|
||||
// Create a new client with message limit
|
||||
const limitedClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxMessages: 3, // Limit messages per connection
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send emails up to and beyond the limit
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Message Limit Test ${i + 1}`,
|
||||
text: `Testing message limits`
|
||||
});
|
||||
|
||||
const result = await limitedClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
// After 3 messages, connection should be refreshed
|
||||
if (i === 2) {
|
||||
console.log('✅ Connection should refresh after message limit');
|
||||
}
|
||||
}
|
||||
|
||||
await limitedClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-05: Connection Reuse - should handle connection state changes', async () => {
|
||||
// Monitor connection state during reuse
|
||||
let connectionEvents = 0;
|
||||
|
||||
const eventClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
eventClient.on('connect', () => connectionEvents++);
|
||||
|
||||
// First email
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'First Email',
|
||||
text: 'Testing connection events'
|
||||
});
|
||||
|
||||
await eventClient.sendMail(email1);
|
||||
const firstConnectCount = connectionEvents;
|
||||
|
||||
// Second email (should reuse connection)
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Second Email',
|
||||
text: 'Should reuse connection'
|
||||
});
|
||||
|
||||
await eventClient.sendMail(email2);
|
||||
|
||||
// Should not have created new connection
|
||||
expect(connectionEvents).toEqual(firstConnectCount);
|
||||
|
||||
await eventClient.close();
|
||||
console.log(`✅ Connection reused (${connectionEvents} total connections)`);
|
||||
});
|
||||
|
||||
tap.test('CCM-05: Connection Reuse - should handle idle connection timeout', async () => {
|
||||
const idleClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 3000 // Short timeout for testing
|
||||
});
|
||||
|
||||
// Send first email
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Pre-idle Email',
|
||||
text: 'Before idle period'
|
||||
});
|
||||
|
||||
await idleClient.sendMail(email1);
|
||||
expect(idleClient.isConnected()).toBeTrue();
|
||||
|
||||
// Wait for potential idle timeout
|
||||
console.log('⏳ Testing idle connection behavior...');
|
||||
await new Promise(resolve => setTimeout(resolve, 4000));
|
||||
|
||||
// Send another email
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Post-idle Email',
|
||||
text: 'After idle period'
|
||||
});
|
||||
|
||||
// Should handle reconnection if needed
|
||||
const result = await idleClient.sendMail(email2);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
await idleClient.close();
|
||||
console.log('✅ Idle connection handling working correctly');
|
||||
});
|
||||
|
||||
tap.test('CCM-05: Connection Reuse - should optimize performance with reuse', async () => {
|
||||
// Compare performance with and without connection reuse
|
||||
|
||||
// Test 1: Multiple connections (no reuse)
|
||||
const noReuseStart = Date.now();
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const tempClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `No Reuse ${i}`,
|
||||
text: 'Testing without reuse'
|
||||
});
|
||||
|
||||
await tempClient.sendMail(email);
|
||||
await tempClient.close();
|
||||
}
|
||||
const noReuseDuration = Date.now() - noReuseStart;
|
||||
|
||||
// Test 2: Single connection (with reuse)
|
||||
const reuseStart = Date.now();
|
||||
const reuseClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `With Reuse ${i}`,
|
||||
text: 'Testing with reuse'
|
||||
});
|
||||
|
||||
await reuseClient.sendMail(email);
|
||||
}
|
||||
|
||||
await reuseClient.close();
|
||||
const reuseDuration = Date.now() - reuseStart;
|
||||
|
||||
console.log(`📊 Performance comparison:`);
|
||||
console.log(` Without reuse: ${noReuseDuration}ms`);
|
||||
console.log(` With reuse: ${reuseDuration}ms`);
|
||||
console.log(` Improvement: ${Math.round((1 - reuseDuration/noReuseDuration) * 100)}%`);
|
||||
|
||||
// Reuse should be faster
|
||||
expect(reuseDuration).toBeLessThan(noReuseDuration);
|
||||
});
|
||||
|
||||
tap.test('CCM-05: Connection Reuse - should handle errors without breaking reuse', async () => {
|
||||
const resilientClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send valid email
|
||||
const validEmail = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Valid Email',
|
||||
text: 'This should work'
|
||||
});
|
||||
|
||||
const result1 = await resilientClient.sendMail(validEmail);
|
||||
expect(result1.success).toBeTrue();
|
||||
|
||||
// Try to send invalid email
|
||||
try {
|
||||
const invalidEmail = new Email({
|
||||
from: 'invalid sender format',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Invalid Email',
|
||||
text: 'This should fail'
|
||||
});
|
||||
await resilientClient.sendMail(invalidEmail);
|
||||
} catch (error) {
|
||||
console.log('✅ Invalid email rejected as expected');
|
||||
}
|
||||
|
||||
// Connection should still be usable
|
||||
const validEmail2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Valid Email After Error',
|
||||
text: 'Connection should still work'
|
||||
});
|
||||
|
||||
const result2 = await resilientClient.sendMail(validEmail2);
|
||||
expect(result2.success).toBeTrue();
|
||||
|
||||
await resilientClient.close();
|
||||
console.log('✅ Connection reuse survived error condition');
|
||||
});
|
||||
|
||||
tap.test('cleanup - close SMTP client', async () => {
|
||||
if (smtpClient && smtpClient.isConnected()) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -0,0 +1,285 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for timeout tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2532,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2532);
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should timeout on unresponsive server', async () => {
|
||||
const startTime = Date.now();
|
||||
let timeoutError = false;
|
||||
|
||||
try {
|
||||
const timeoutClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: 9999, // Non-existent port
|
||||
secure: false,
|
||||
connectionTimeout: 2000, // 2 second timeout
|
||||
debug: true
|
||||
});
|
||||
|
||||
await timeoutClient.verify();
|
||||
} catch (error: any) {
|
||||
timeoutError = true;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(error).toBeInstanceOf(Error);
|
||||
expect(duration).toBeLessThan(3000); // Should timeout within 3s
|
||||
expect(duration).toBeGreaterThan(1500); // But not too early
|
||||
|
||||
console.log(`✅ Connection timeout after ${duration}ms`);
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
|
||||
expect(timeoutError).toBeTrue();
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should handle slow server response', async () => {
|
||||
// Create a mock slow server
|
||||
const slowServer = net.createServer((socket) => {
|
||||
// Accept connection but delay response
|
||||
setTimeout(() => {
|
||||
socket.write('220 Slow server ready\r\n');
|
||||
}, 3000); // 3 second delay
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
slowServer.listen(2533, () => resolve());
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
let timeoutOccurred = false;
|
||||
|
||||
try {
|
||||
const slowClient = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2533,
|
||||
secure: false,
|
||||
connectionTimeout: 1000, // 1 second timeout
|
||||
debug: true
|
||||
});
|
||||
|
||||
await slowClient.verify();
|
||||
} catch (error: any) {
|
||||
timeoutOccurred = true;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(duration).toBeLessThan(2000);
|
||||
console.log(`✅ Slow server timeout after ${duration}ms`);
|
||||
}
|
||||
|
||||
expect(timeoutOccurred).toBeTrue();
|
||||
|
||||
slowServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should respect socket timeout during data transfer', async () => {
|
||||
const socketTimeoutClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 10000, // 10 second socket timeout
|
||||
debug: true
|
||||
});
|
||||
|
||||
await socketTimeoutClient.verify();
|
||||
|
||||
// Send a normal email
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Socket Timeout Test',
|
||||
text: 'Testing socket timeout configuration'
|
||||
});
|
||||
|
||||
const result = await socketTimeoutClient.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
await socketTimeoutClient.close();
|
||||
console.log('✅ Socket timeout configuration applied');
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should handle timeout during TLS handshake', async () => {
|
||||
// Create a server that accepts connections but doesn't complete TLS
|
||||
const badTlsServer = net.createServer((socket) => {
|
||||
// Accept connection but don't respond to TLS
|
||||
socket.on('data', () => {
|
||||
// Do nothing - simulate hung TLS handshake
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
badTlsServer.listen(2534, () => resolve());
|
||||
});
|
||||
|
||||
let tlsTimeoutError = false;
|
||||
const startTime = Date.now();
|
||||
|
||||
try {
|
||||
const tlsTimeoutClient = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2534,
|
||||
secure: true, // Try TLS
|
||||
connectionTimeout: 2000,
|
||||
tls: {
|
||||
rejectUnauthorized: false
|
||||
}
|
||||
});
|
||||
|
||||
await tlsTimeoutClient.verify();
|
||||
} catch (error: any) {
|
||||
tlsTimeoutError = true;
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(duration).toBeLessThan(3000);
|
||||
console.log(`✅ TLS handshake timeout after ${duration}ms`);
|
||||
}
|
||||
|
||||
expect(tlsTimeoutError).toBeTrue();
|
||||
|
||||
badTlsServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should not timeout on successful quick connection', async () => {
|
||||
const quickClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 30000, // Very long timeout
|
||||
debug: true
|
||||
});
|
||||
|
||||
const startTime = Date.now();
|
||||
|
||||
const isConnected = await quickClient.verify();
|
||||
const duration = Date.now() - startTime;
|
||||
|
||||
expect(isConnected).toBeTrue();
|
||||
expect(duration).toBeLessThan(5000); // Should connect quickly
|
||||
|
||||
await quickClient.close();
|
||||
console.log(`✅ Quick connection established in ${duration}ms`);
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should handle timeout during authentication', async () => {
|
||||
// Start auth server
|
||||
const authServer = await startTestServer({
|
||||
port: 2535,
|
||||
authRequired: true
|
||||
});
|
||||
|
||||
// Create mock auth that delays
|
||||
const authTimeoutClient = createSmtpClient({
|
||||
host: authServer.hostname,
|
||||
port: authServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 1000, // Very short socket timeout
|
||||
auth: {
|
||||
user: 'testuser',
|
||||
pass: 'testpass'
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
await authTimeoutClient.verify();
|
||||
// If this succeeds, auth was fast enough
|
||||
await authTimeoutClient.close();
|
||||
console.log('✅ Authentication completed within timeout');
|
||||
} catch (error) {
|
||||
console.log('✅ Authentication timeout handled');
|
||||
}
|
||||
|
||||
await stopTestServer(authServer);
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should apply different timeouts for different operations', async () => {
|
||||
const multiTimeoutClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000, // Connection establishment
|
||||
socketTimeout: 30000, // Data operations
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Connection should be quick
|
||||
const connectStart = Date.now();
|
||||
await multiTimeoutClient.verify();
|
||||
const connectDuration = Date.now() - connectStart;
|
||||
|
||||
expect(connectDuration).toBeLessThan(5000);
|
||||
|
||||
// Send email with potentially longer operation
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Multi-timeout Test',
|
||||
text: 'Testing different timeout values',
|
||||
attachments: [{
|
||||
filename: 'test.txt',
|
||||
content: Buffer.from('Test content'),
|
||||
contentType: 'text/plain'
|
||||
}]
|
||||
});
|
||||
|
||||
const sendStart = Date.now();
|
||||
const result = await multiTimeoutClient.sendMail(email);
|
||||
const sendDuration = Date.now() - sendStart;
|
||||
|
||||
expect(result.success).toBeTrue();
|
||||
console.log(`✅ Different timeouts applied: connect=${connectDuration}ms, send=${sendDuration}ms`);
|
||||
|
||||
await multiTimeoutClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-06: Connection Timeout - should retry after timeout with pooled connections', async () => {
|
||||
const retryClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
pool: true,
|
||||
maxConnections: 2,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// First connection should succeed
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Pre-timeout Email',
|
||||
text: 'Before any timeout'
|
||||
});
|
||||
|
||||
const result1 = await retryClient.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
|
||||
// Pool should handle connection management
|
||||
const poolStatus = retryClient.getPoolStatus();
|
||||
console.log('📊 Pool status:', poolStatus);
|
||||
|
||||
await retryClient.close();
|
||||
console.log('✅ Connection pool handles timeouts gracefully');
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
@ -0,0 +1,308 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestServer, stopTestServer, type ITestServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient, createPooledSmtpClient } from '../../../ts/mail/delivery/smtpclient/index.js';
|
||||
import type { SmtpClient } from '../../../ts/mail/delivery/smtpclient/smtp-client.js';
|
||||
import { Email } from '../../../ts/mail/core/classes.email.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: ITestServer;
|
||||
|
||||
tap.test('setup - start SMTP server for reconnection tests', async () => {
|
||||
testServer = await startTestServer({
|
||||
port: 2533,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
expect(testServer.port).toEqual(2533);
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - should reconnect after connection loss', async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// First connection and email
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Before Disconnect',
|
||||
text: 'First email before connection loss'
|
||||
});
|
||||
|
||||
const result1 = await client.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
expect(client.isConnected()).toBeTrue();
|
||||
|
||||
// Force disconnect
|
||||
await client.close();
|
||||
expect(client.isConnected()).toBeFalse();
|
||||
|
||||
// Try to send another email - should auto-reconnect
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'After Reconnect',
|
||||
text: 'Email after automatic reconnection'
|
||||
});
|
||||
|
||||
const result2 = await client.sendMail(email2);
|
||||
expect(result2.success).toBeTrue();
|
||||
expect(client.isConnected()).toBeTrue();
|
||||
|
||||
await client.close();
|
||||
console.log('✅ Automatic reconnection successful');
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - pooled client should reconnect failed connections', async () => {
|
||||
const pooledClient = createPooledSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
maxConnections: 3,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Send emails to establish pool connections
|
||||
const promises = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: `recipient${i}@example.com`,
|
||||
subject: `Pool Test ${i}`,
|
||||
text: 'Testing connection pool'
|
||||
});
|
||||
promises.push(pooledClient.sendMail(email));
|
||||
}
|
||||
|
||||
await Promise.all(promises);
|
||||
|
||||
const poolStatus1 = pooledClient.getPoolStatus();
|
||||
console.log('📊 Pool status before disruption:', poolStatus1);
|
||||
|
||||
// Send more emails - pool should handle any connection issues
|
||||
const promises2 = [];
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: `recipient${i}@example.com`,
|
||||
subject: `Pool Recovery ${i}`,
|
||||
text: 'Testing pool recovery'
|
||||
});
|
||||
promises2.push(pooledClient.sendMail(email));
|
||||
}
|
||||
|
||||
const results = await Promise.all(promises2);
|
||||
results.forEach(result => {
|
||||
expect(result.success).toBeTrue();
|
||||
});
|
||||
|
||||
const poolStatus2 = pooledClient.getPoolStatus();
|
||||
console.log('📊 Pool status after recovery:', poolStatus2);
|
||||
|
||||
await pooledClient.close();
|
||||
console.log('✅ Connection pool handles reconnection automatically');
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - should handle server restart', async () => {
|
||||
// Create client
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send first email
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'Before Server Restart',
|
||||
text: 'Email before server restart'
|
||||
});
|
||||
|
||||
const result1 = await client.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
|
||||
// Simulate server restart
|
||||
console.log('🔄 Simulating server restart...');
|
||||
await stopTestServer(testServer);
|
||||
await new Promise(resolve => setTimeout(resolve, 1000));
|
||||
|
||||
// Restart server on same port
|
||||
testServer = await startTestServer({
|
||||
port: 2533,
|
||||
tlsEnabled: false,
|
||||
authRequired: false
|
||||
});
|
||||
|
||||
// Try to send another email
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'After Server Restart',
|
||||
text: 'Email after server restart'
|
||||
});
|
||||
|
||||
const result2 = await client.sendMail(email2);
|
||||
expect(result2.success).toBeTrue();
|
||||
|
||||
await client.close();
|
||||
console.log('✅ Client recovered from server restart');
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - should handle network interruption', async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 10000
|
||||
});
|
||||
|
||||
// Establish connection
|
||||
await client.verify();
|
||||
|
||||
// Send emails with simulated network issues
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Network Test ${i}`,
|
||||
text: `Testing network resilience ${i}`
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await client.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
console.log(`✅ Email ${i + 1} sent successfully`);
|
||||
} catch (error) {
|
||||
console.log(`⚠️ Email ${i + 1} failed, will retry`);
|
||||
// Client should recover on next attempt
|
||||
}
|
||||
|
||||
// Add small delay between sends
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
}
|
||||
|
||||
await client.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - should limit reconnection attempts', async () => {
|
||||
// Connect to a port that will be closed
|
||||
const tempServer = net.createServer();
|
||||
await new Promise<void>((resolve) => {
|
||||
tempServer.listen(2534, () => resolve());
|
||||
});
|
||||
|
||||
const client = createSmtpClient({
|
||||
host: 'localhost',
|
||||
port: 2534,
|
||||
secure: false,
|
||||
connectionTimeout: 2000
|
||||
});
|
||||
|
||||
// Close the server to simulate failure
|
||||
tempServer.close();
|
||||
await new Promise(resolve => setTimeout(resolve, 100));
|
||||
|
||||
let errorCount = 0;
|
||||
const maxAttempts = 3;
|
||||
|
||||
// Try multiple times
|
||||
for (let i = 0; i < maxAttempts; i++) {
|
||||
try {
|
||||
await client.verify();
|
||||
} catch (error) {
|
||||
errorCount++;
|
||||
}
|
||||
}
|
||||
|
||||
expect(errorCount).toEqual(maxAttempts);
|
||||
console.log('✅ Reconnection attempts are limited to prevent infinite loops');
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - should maintain state after reconnect', async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Send email with specific settings
|
||||
const email1 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'State Test 1',
|
||||
text: 'Testing state persistence',
|
||||
priority: 'high',
|
||||
headers: {
|
||||
'X-Test-ID': 'test-123'
|
||||
}
|
||||
});
|
||||
|
||||
const result1 = await client.sendMail(email1);
|
||||
expect(result1.success).toBeTrue();
|
||||
|
||||
// Force reconnection
|
||||
await client.close();
|
||||
|
||||
// Send another email - client state should be maintained
|
||||
const email2 = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: 'State Test 2',
|
||||
text: 'After reconnection',
|
||||
priority: 'high',
|
||||
headers: {
|
||||
'X-Test-ID': 'test-456'
|
||||
}
|
||||
});
|
||||
|
||||
const result2 = await client.sendMail(email2);
|
||||
expect(result2.success).toBeTrue();
|
||||
|
||||
await client.close();
|
||||
console.log('✅ Client state maintained after reconnection');
|
||||
});
|
||||
|
||||
tap.test('CCM-07: Automatic Reconnection - should handle rapid reconnections', async () => {
|
||||
const client = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000
|
||||
});
|
||||
|
||||
// Rapid connect/disconnect cycles
|
||||
for (let i = 0; i < 5; i++) {
|
||||
const email = new Email({
|
||||
from: 'sender@example.com',
|
||||
to: 'recipient@example.com',
|
||||
subject: `Rapid Test ${i}`,
|
||||
text: 'Testing rapid reconnections'
|
||||
});
|
||||
|
||||
const result = await client.sendMail(email);
|
||||
expect(result.success).toBeTrue();
|
||||
|
||||
// Force disconnect
|
||||
await client.close();
|
||||
|
||||
// No delay - immediate next attempt
|
||||
}
|
||||
|
||||
console.log('✅ Rapid reconnections handled successfully');
|
||||
});
|
||||
|
||||
tap.test('cleanup - stop SMTP server', async () => {
|
||||
await stopTestServer(testServer);
|
||||
});
|
||||
|
||||
tap.start();
|
135
test/suite/smtpclient_connection/test.ccm-08.dns-resolution.ts
Normal file
135
test/suite/smtpclient_connection/test.ccm-08.dns-resolution.ts
Normal file
@ -0,0 +1,135 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import * as dns from 'dns';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const resolveMx = promisify(dns.resolveMx);
|
||||
const resolve4 = promisify(dns.resolve4);
|
||||
const resolve6 = promisify(dns.resolve6);
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCM-08: DNS resolution and MX record lookup', async () => {
|
||||
// Test basic DNS resolution
|
||||
try {
|
||||
const ipv4Addresses = await resolve4('example.com');
|
||||
expect(ipv4Addresses).toBeArray();
|
||||
expect(ipv4Addresses.length).toBeGreaterThan(0);
|
||||
console.log('IPv4 addresses for example.com:', ipv4Addresses);
|
||||
} catch (error) {
|
||||
console.log('IPv4 resolution failed (may be expected in test environment):', error.message);
|
||||
}
|
||||
|
||||
// Test IPv6 resolution
|
||||
try {
|
||||
const ipv6Addresses = await resolve6('example.com');
|
||||
expect(ipv6Addresses).toBeArray();
|
||||
console.log('IPv6 addresses for example.com:', ipv6Addresses);
|
||||
} catch (error) {
|
||||
console.log('IPv6 resolution failed (common for many domains):', error.message);
|
||||
}
|
||||
|
||||
// Test MX record lookup
|
||||
try {
|
||||
const mxRecords = await resolveMx('example.com');
|
||||
expect(mxRecords).toBeArray();
|
||||
if (mxRecords.length > 0) {
|
||||
expect(mxRecords[0]).toHaveProperty('priority');
|
||||
expect(mxRecords[0]).toHaveProperty('exchange');
|
||||
console.log('MX records for example.com:', mxRecords);
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('MX record lookup failed (may be expected in test environment):', error.message);
|
||||
}
|
||||
|
||||
// Test local resolution (should work in test environment)
|
||||
try {
|
||||
const localhostIpv4 = await resolve4('localhost');
|
||||
expect(localhostIpv4).toContain('127.0.0.1');
|
||||
} catch (error) {
|
||||
// Fallback for environments where localhost doesn't resolve via DNS
|
||||
console.log('Localhost DNS resolution not available, using direct IP');
|
||||
}
|
||||
|
||||
// Test invalid domain handling
|
||||
try {
|
||||
await resolve4('this-domain-definitely-does-not-exist-12345.com');
|
||||
expect(true).toBeFalsy(); // Should not reach here
|
||||
} catch (error) {
|
||||
expect(error.code).toMatch(/ENOTFOUND|ENODATA/);
|
||||
}
|
||||
|
||||
// Test MX record priority sorting
|
||||
const mockMxRecords = [
|
||||
{ priority: 20, exchange: 'mx2.example.com' },
|
||||
{ priority: 10, exchange: 'mx1.example.com' },
|
||||
{ priority: 30, exchange: 'mx3.example.com' }
|
||||
];
|
||||
|
||||
const sortedRecords = mockMxRecords.sort((a, b) => a.priority - b.priority);
|
||||
expect(sortedRecords[0].exchange).toEqual('mx1.example.com');
|
||||
expect(sortedRecords[1].exchange).toEqual('mx2.example.com');
|
||||
expect(sortedRecords[2].exchange).toEqual('mx3.example.com');
|
||||
});
|
||||
|
||||
tap.test('CCM-08: DNS caching behavior', async () => {
|
||||
const startTime = Date.now();
|
||||
|
||||
// First resolution (cold cache)
|
||||
try {
|
||||
await resolve4('example.com');
|
||||
} catch (error) {
|
||||
// Ignore errors, we're testing timing
|
||||
}
|
||||
|
||||
const firstResolutionTime = Date.now() - startTime;
|
||||
|
||||
// Second resolution (potentially cached)
|
||||
const secondStartTime = Date.now();
|
||||
try {
|
||||
await resolve4('example.com');
|
||||
} catch (error) {
|
||||
// Ignore errors, we're testing timing
|
||||
}
|
||||
|
||||
const secondResolutionTime = Date.now() - secondStartTime;
|
||||
|
||||
console.log(`First resolution: ${firstResolutionTime}ms, Second resolution: ${secondResolutionTime}ms`);
|
||||
|
||||
// Note: We can't guarantee caching behavior in all environments
|
||||
// so we just log the times for manual inspection
|
||||
});
|
||||
|
||||
tap.test('CCM-08: Multiple A record handling', async () => {
|
||||
// Test handling of domains with multiple A records
|
||||
try {
|
||||
const googleIps = await resolve4('google.com');
|
||||
if (googleIps.length > 1) {
|
||||
expect(googleIps).toBeArray();
|
||||
expect(googleIps.length).toBeGreaterThan(1);
|
||||
console.log('Multiple A records found for google.com:', googleIps);
|
||||
|
||||
// Verify all are valid IPv4 addresses
|
||||
const ipv4Regex = /^(\d{1,3}\.){3}\d{1,3}$/;
|
||||
for (const ip of googleIps) {
|
||||
expect(ip).toMatch(ipv4Regex);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('Could not resolve google.com:', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
201
test/suite/smtpclient_connection/test.ccm-09.ipv6-dual-stack.ts
Normal file
201
test/suite/smtpclient_connection/test.ccm-09.ipv6-dual-stack.ts
Normal file
@ -0,0 +1,201 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import * as net from 'net';
|
||||
import * as os from 'os';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCM-09: Check system IPv6 support', async () => {
|
||||
const networkInterfaces = os.networkInterfaces();
|
||||
let hasIPv6 = false;
|
||||
|
||||
for (const interfaceName in networkInterfaces) {
|
||||
const interfaces = networkInterfaces[interfaceName];
|
||||
if (interfaces) {
|
||||
for (const iface of interfaces) {
|
||||
if (iface.family === 'IPv6' && !iface.internal) {
|
||||
hasIPv6 = true;
|
||||
console.log(`Found IPv6 address: ${iface.address} on ${interfaceName}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`System has IPv6 support: ${hasIPv6}`);
|
||||
});
|
||||
|
||||
tap.test('CCM-09: IPv4 connection test', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '127.0.0.1', // Explicit IPv4
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
let connected = false;
|
||||
let connectionFamily = '';
|
||||
|
||||
smtpClient.on('connection', (info: any) => {
|
||||
connected = true;
|
||||
if (info && info.socket) {
|
||||
connectionFamily = info.socket.remoteFamily || '';
|
||||
}
|
||||
});
|
||||
|
||||
smtpClient.on('error', (error: Error) => {
|
||||
console.error('IPv4 connection error:', error.message);
|
||||
});
|
||||
|
||||
// Test connection
|
||||
const result = await smtpClient.connect();
|
||||
expect(result).toBeTruthy();
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
console.log(`Connected via IPv4, family: ${connectionFamily}`);
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-09: IPv6 connection test (if supported)', async () => {
|
||||
// Check if IPv6 is available
|
||||
const hasIPv6 = await new Promise<boolean>((resolve) => {
|
||||
const testSocket = net.createConnection({
|
||||
host: '::1',
|
||||
port: 1, // Any port, will fail but tells us if IPv6 works
|
||||
timeout: 100
|
||||
});
|
||||
|
||||
testSocket.on('error', (err: any) => {
|
||||
// ECONNREFUSED means IPv6 works but port is closed (expected)
|
||||
// ENETUNREACH or EAFNOSUPPORT means IPv6 not available
|
||||
resolve(err.code === 'ECONNREFUSED');
|
||||
});
|
||||
|
||||
testSocket.on('connect', () => {
|
||||
testSocket.end();
|
||||
resolve(true);
|
||||
});
|
||||
});
|
||||
|
||||
if (!hasIPv6) {
|
||||
console.log('IPv6 not available on this system, skipping IPv6 tests');
|
||||
return;
|
||||
}
|
||||
|
||||
// Try IPv6 connection
|
||||
const smtpClient = createSmtpClient({
|
||||
host: '::1', // IPv6 loopback
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
let connected = false;
|
||||
let connectionFamily = '';
|
||||
|
||||
smtpClient.on('connection', (info: any) => {
|
||||
connected = true;
|
||||
if (info && info.socket) {
|
||||
connectionFamily = info.socket.remoteFamily || '';
|
||||
}
|
||||
});
|
||||
|
||||
smtpClient.on('error', (error: Error) => {
|
||||
console.error('IPv6 connection error:', error.message);
|
||||
});
|
||||
|
||||
try {
|
||||
const result = await smtpClient.connect();
|
||||
if (result && smtpClient.isConnected()) {
|
||||
console.log(`Connected via IPv6, family: ${connectionFamily}`);
|
||||
await smtpClient.close();
|
||||
}
|
||||
} catch (error) {
|
||||
console.log('IPv6 connection failed (server may not support IPv6):', error.message);
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-09: Hostname resolution preference', async () => {
|
||||
// Test that client can handle hostnames that resolve to both IPv4 and IPv6
|
||||
const smtpClient = createSmtpClient({
|
||||
host: 'localhost', // Should resolve to both 127.0.0.1 and ::1
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 5000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
let connectionInfo: any = null;
|
||||
|
||||
smtpClient.on('connection', (info: any) => {
|
||||
connectionInfo = info;
|
||||
});
|
||||
|
||||
const result = await smtpClient.connect();
|
||||
expect(result).toBeTruthy();
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
if (connectionInfo && connectionInfo.socket) {
|
||||
console.log(`Connected to localhost via ${connectionInfo.socket.remoteFamily || 'unknown'}`);
|
||||
console.log(`Local address: ${connectionInfo.socket.localAddress}`);
|
||||
console.log(`Remote address: ${connectionInfo.socket.remoteAddress}`);
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-09: Happy Eyeballs algorithm simulation', async () => {
|
||||
// Test connecting to multiple addresses with preference
|
||||
const addresses = ['127.0.0.1', '::1', 'localhost'];
|
||||
const results: Array<{ address: string; time: number; success: boolean }> = [];
|
||||
|
||||
for (const address of addresses) {
|
||||
const startTime = Date.now();
|
||||
const smtpClient = createSmtpClient({
|
||||
host: address,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
connectionTimeout: 1000,
|
||||
debug: false
|
||||
});
|
||||
|
||||
try {
|
||||
const connected = await smtpClient.connect();
|
||||
const elapsed = Date.now() - startTime;
|
||||
results.push({ address, time: elapsed, success: !!connected });
|
||||
|
||||
if (connected) {
|
||||
await smtpClient.close();
|
||||
}
|
||||
} catch (error) {
|
||||
const elapsed = Date.now() - startTime;
|
||||
results.push({ address, time: elapsed, success: false });
|
||||
}
|
||||
}
|
||||
|
||||
console.log('Connection race results:');
|
||||
results.forEach(r => {
|
||||
console.log(` ${r.address}: ${r.success ? 'SUCCESS' : 'FAILED'} in ${r.time}ms`);
|
||||
});
|
||||
|
||||
// At least one should succeed
|
||||
const successfulConnections = results.filter(r => r.success);
|
||||
expect(successfulConnections.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
298
test/suite/smtpclient_connection/test.ccm-10.proxy-support.ts
Normal file
298
test/suite/smtpclient_connection/test.ccm-10.proxy-support.ts
Normal file
@ -0,0 +1,298 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import * as net from 'net';
|
||||
import * as http from 'http';
|
||||
|
||||
let testServer: any;
|
||||
let proxyServer: http.Server;
|
||||
let socksProxyServer: net.Server;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer();
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Setup HTTP CONNECT proxy', async () => {
|
||||
// Create a simple HTTP CONNECT proxy
|
||||
proxyServer = http.createServer();
|
||||
|
||||
proxyServer.on('connect', (req, clientSocket, head) => {
|
||||
console.log(`Proxy CONNECT request to ${req.url}`);
|
||||
|
||||
const [host, port] = req.url!.split(':');
|
||||
const serverSocket = net.connect(parseInt(port), host, () => {
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n' +
|
||||
'Proxy-agent: Test-Proxy\r\n' +
|
||||
'\r\n');
|
||||
|
||||
// Pipe data between client and server
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
});
|
||||
|
||||
serverSocket.on('error', (err) => {
|
||||
console.error('Proxy server socket error:', err);
|
||||
clientSocket.end();
|
||||
});
|
||||
|
||||
clientSocket.on('error', (err) => {
|
||||
console.error('Proxy client socket error:', err);
|
||||
serverSocket.end();
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
proxyServer.listen(0, '127.0.0.1', () => {
|
||||
const address = proxyServer.address() as net.AddressInfo;
|
||||
console.log(`HTTP proxy listening on port ${address.port}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Test connection through HTTP proxy', async () => {
|
||||
const proxyAddress = proxyServer.address() as net.AddressInfo;
|
||||
|
||||
// Note: Real SMTP clients would need proxy configuration
|
||||
// This simulates what a proxy-aware SMTP client would do
|
||||
const proxyOptions = {
|
||||
host: proxyAddress.address,
|
||||
port: proxyAddress.port,
|
||||
method: 'CONNECT',
|
||||
path: `127.0.0.1:${testServer.port}`,
|
||||
headers: {
|
||||
'Proxy-Authorization': 'Basic dGVzdDp0ZXN0' // test:test in base64
|
||||
}
|
||||
};
|
||||
|
||||
const connected = await new Promise<boolean>((resolve) => {
|
||||
const req = http.request(proxyOptions);
|
||||
|
||||
req.on('connect', (res, socket, head) => {
|
||||
console.log('Connected through proxy, status:', res.statusCode);
|
||||
expect(res.statusCode).toEqual(200);
|
||||
|
||||
// Now we have a raw socket to the SMTP server through the proxy
|
||||
socket.on('data', (data) => {
|
||||
const response = data.toString();
|
||||
console.log('SMTP response through proxy:', response.trim());
|
||||
if (response.includes('220')) {
|
||||
socket.write('QUIT\r\n');
|
||||
socket.end();
|
||||
resolve(true);
|
||||
}
|
||||
});
|
||||
|
||||
socket.on('error', (err) => {
|
||||
console.error('Socket error:', err);
|
||||
resolve(false);
|
||||
});
|
||||
});
|
||||
|
||||
req.on('error', (err) => {
|
||||
console.error('Proxy request error:', err);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(connected).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Test SOCKS5 proxy simulation', async () => {
|
||||
// Create a minimal SOCKS5 proxy for testing
|
||||
socksProxyServer = net.createServer((clientSocket) => {
|
||||
let authenticated = false;
|
||||
let targetHost: string;
|
||||
let targetPort: number;
|
||||
|
||||
clientSocket.on('data', (data) => {
|
||||
if (!authenticated) {
|
||||
// SOCKS5 handshake
|
||||
if (data[0] === 0x05) { // SOCKS version 5
|
||||
// Send back: no authentication required
|
||||
clientSocket.write(Buffer.from([0x05, 0x00]));
|
||||
authenticated = true;
|
||||
}
|
||||
} else if (!targetHost) {
|
||||
// Connection request
|
||||
if (data[0] === 0x05 && data[1] === 0x01) { // CONNECT command
|
||||
const addressType = data[3];
|
||||
|
||||
if (addressType === 0x01) { // IPv4
|
||||
targetHost = `${data[4]}.${data[5]}.${data[6]}.${data[7]}`;
|
||||
targetPort = (data[8] << 8) + data[9];
|
||||
|
||||
// Connect to target
|
||||
const serverSocket = net.connect(targetPort, targetHost, () => {
|
||||
// Send success response
|
||||
const response = Buffer.alloc(10);
|
||||
response[0] = 0x05; // SOCKS version
|
||||
response[1] = 0x00; // Success
|
||||
response[2] = 0x00; // Reserved
|
||||
response[3] = 0x01; // IPv4
|
||||
response[4] = data[4]; // Copy address
|
||||
response[5] = data[5];
|
||||
response[6] = data[6];
|
||||
response[7] = data[7];
|
||||
response[8] = data[8]; // Copy port
|
||||
response[9] = data[9];
|
||||
|
||||
clientSocket.write(response);
|
||||
|
||||
// Start proxying
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
});
|
||||
|
||||
serverSocket.on('error', (err) => {
|
||||
console.error('SOCKS target connection error:', err);
|
||||
clientSocket.end();
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
clientSocket.on('error', (err) => {
|
||||
console.error('SOCKS client error:', err);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
socksProxyServer.listen(0, '127.0.0.1', () => {
|
||||
const address = socksProxyServer.address() as net.AddressInfo;
|
||||
console.log(`SOCKS5 proxy listening on port ${address.port}`);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
// Test connection through SOCKS proxy
|
||||
const socksAddress = socksProxyServer.address() as net.AddressInfo;
|
||||
const socksClient = net.connect(socksAddress.port, socksAddress.address);
|
||||
|
||||
const connected = await new Promise<boolean>((resolve) => {
|
||||
let phase = 'handshake';
|
||||
|
||||
socksClient.on('connect', () => {
|
||||
// Send SOCKS5 handshake
|
||||
socksClient.write(Buffer.from([0x05, 0x01, 0x00])); // Version 5, 1 method, no auth
|
||||
});
|
||||
|
||||
socksClient.on('data', (data) => {
|
||||
if (phase === 'handshake' && data[0] === 0x05 && data[1] === 0x00) {
|
||||
phase = 'connect';
|
||||
// Send connection request
|
||||
const connectReq = Buffer.alloc(10);
|
||||
connectReq[0] = 0x05; // SOCKS version
|
||||
connectReq[1] = 0x01; // CONNECT
|
||||
connectReq[2] = 0x00; // Reserved
|
||||
connectReq[3] = 0x01; // IPv4
|
||||
connectReq[4] = 127; // 127.0.0.1
|
||||
connectReq[5] = 0;
|
||||
connectReq[6] = 0;
|
||||
connectReq[7] = 1;
|
||||
connectReq[8] = (testServer.port >> 8) & 0xFF; // Port high byte
|
||||
connectReq[9] = testServer.port & 0xFF; // Port low byte
|
||||
|
||||
socksClient.write(connectReq);
|
||||
} else if (phase === 'connect' && data[0] === 0x05 && data[1] === 0x00) {
|
||||
phase = 'connected';
|
||||
console.log('Connected through SOCKS5 proxy');
|
||||
// Now we're connected to the SMTP server
|
||||
} else if (phase === 'connected') {
|
||||
const response = data.toString();
|
||||
console.log('SMTP response through SOCKS:', response.trim());
|
||||
if (response.includes('220')) {
|
||||
socksClient.write('QUIT\r\n');
|
||||
socksClient.end();
|
||||
resolve(true);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
socksClient.on('error', (err) => {
|
||||
console.error('SOCKS client error:', err);
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
setTimeout(() => resolve(false), 5000); // Timeout after 5 seconds
|
||||
});
|
||||
|
||||
expect(connected).toBeTruthy();
|
||||
});
|
||||
|
||||
tap.test('CCM-10: Test proxy authentication failure', async () => {
|
||||
// Create a proxy that requires authentication
|
||||
const authProxyServer = http.createServer();
|
||||
|
||||
authProxyServer.on('connect', (req, clientSocket, head) => {
|
||||
const authHeader = req.headers['proxy-authorization'];
|
||||
|
||||
if (!authHeader || authHeader !== 'Basic dGVzdDp0ZXN0') {
|
||||
clientSocket.write('HTTP/1.1 407 Proxy Authentication Required\r\n' +
|
||||
'Proxy-Authenticate: Basic realm="Test Proxy"\r\n' +
|
||||
'\r\n');
|
||||
clientSocket.end();
|
||||
return;
|
||||
}
|
||||
|
||||
// Authentication successful, proceed with connection
|
||||
const [host, port] = req.url!.split(':');
|
||||
const serverSocket = net.connect(parseInt(port), host, () => {
|
||||
clientSocket.write('HTTP/1.1 200 Connection Established\r\n\r\n');
|
||||
serverSocket.pipe(clientSocket);
|
||||
clientSocket.pipe(serverSocket);
|
||||
});
|
||||
});
|
||||
|
||||
await new Promise<void>((resolve) => {
|
||||
authProxyServer.listen(0, '127.0.0.1', () => {
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
const authProxyAddress = authProxyServer.address() as net.AddressInfo;
|
||||
|
||||
// Test without authentication
|
||||
const failedAuth = await new Promise<boolean>((resolve) => {
|
||||
const req = http.request({
|
||||
host: authProxyAddress.address,
|
||||
port: authProxyAddress.port,
|
||||
method: 'CONNECT',
|
||||
path: `127.0.0.1:${testServer.port}`
|
||||
});
|
||||
|
||||
req.on('connect', () => resolve(false));
|
||||
req.on('response', (res) => {
|
||||
expect(res.statusCode).toEqual(407);
|
||||
resolve(true);
|
||||
});
|
||||
req.on('error', () => resolve(false));
|
||||
|
||||
req.end();
|
||||
});
|
||||
|
||||
expect(failedAuth).toBeTruthy();
|
||||
|
||||
authProxyServer.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test servers', async () => {
|
||||
if (proxyServer) {
|
||||
await new Promise<void>((resolve) => proxyServer.close(() => resolve()));
|
||||
}
|
||||
|
||||
if (socksProxyServer) {
|
||||
await new Promise<void>((resolve) => socksProxyServer.close(() => resolve()));
|
||||
}
|
||||
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
284
test/suite/smtpclient_connection/test.ccm-11.keepalive.ts
Normal file
284
test/suite/smtpclient_connection/test.ccm-11.keepalive.ts
Normal file
@ -0,0 +1,284 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import { startTestSmtpServer } from '../../helpers/server.loader.js';
|
||||
import { createSmtpClient } from '../../helpers/smtp.client.js';
|
||||
import * as net from 'net';
|
||||
|
||||
let testServer: any;
|
||||
|
||||
tap.test('setup test SMTP server', async () => {
|
||||
testServer = await startTestSmtpServer({
|
||||
socketTimeout: 30000 // 30 second timeout for keep-alive tests
|
||||
});
|
||||
expect(testServer).toBeTruthy();
|
||||
expect(testServer.port).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
tap.test('CCM-11: Basic keep-alive functionality', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
keepAlive: true,
|
||||
keepAliveInterval: 5000, // 5 seconds
|
||||
connectionTimeout: 10000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
// Connect to server
|
||||
const connected = await smtpClient.connect();
|
||||
expect(connected).toBeTruthy();
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
// Track keep-alive activity
|
||||
let keepAliveCount = 0;
|
||||
let lastActivity = Date.now();
|
||||
|
||||
smtpClient.on('keepalive', () => {
|
||||
keepAliveCount++;
|
||||
const elapsed = Date.now() - lastActivity;
|
||||
console.log(`Keep-alive sent after ${elapsed}ms`);
|
||||
lastActivity = Date.now();
|
||||
});
|
||||
|
||||
// Wait for multiple keep-alive cycles
|
||||
await new Promise(resolve => setTimeout(resolve, 12000)); // Wait 12 seconds
|
||||
|
||||
// Should have sent at least 2 keep-alive messages
|
||||
expect(keepAliveCount).toBeGreaterThanOrEqual(2);
|
||||
|
||||
// Connection should still be alive
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-11: Keep-alive with NOOP command', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
keepAlive: true,
|
||||
keepAliveInterval: 3000,
|
||||
connectionTimeout: 10000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
let noopResponses = 0;
|
||||
|
||||
// Send NOOP commands manually to simulate keep-alive
|
||||
for (let i = 0; i < 3; i++) {
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
try {
|
||||
const response = await smtpClient.sendCommand('NOOP');
|
||||
if (response && response.includes('250')) {
|
||||
noopResponses++;
|
||||
console.log(`NOOP response ${i + 1}: ${response.trim()}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('NOOP error:', error.message);
|
||||
}
|
||||
}
|
||||
|
||||
expect(noopResponses).toEqual(3);
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-11: Connection idle timeout without keep-alive', async () => {
|
||||
// Create a client without keep-alive
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
keepAlive: false, // Disabled
|
||||
connectionTimeout: 5000,
|
||||
socketTimeout: 5000, // 5 second socket timeout
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
let disconnected = false;
|
||||
let timeoutError = false;
|
||||
|
||||
smtpClient.on('timeout', () => {
|
||||
timeoutError = true;
|
||||
console.log('Socket timeout detected');
|
||||
});
|
||||
|
||||
smtpClient.on('close', () => {
|
||||
disconnected = true;
|
||||
console.log('Connection closed');
|
||||
});
|
||||
|
||||
smtpClient.on('error', (error: Error) => {
|
||||
console.log('Connection error:', error.message);
|
||||
if (error.message.includes('timeout')) {
|
||||
timeoutError = true;
|
||||
}
|
||||
});
|
||||
|
||||
// Wait for timeout (longer than socket timeout)
|
||||
await new Promise(resolve => setTimeout(resolve, 7000));
|
||||
|
||||
// Without keep-alive, connection might timeout
|
||||
// This depends on server configuration
|
||||
if (disconnected || timeoutError) {
|
||||
console.log('Connection timed out as expected without keep-alive');
|
||||
expect(disconnected || timeoutError).toBeTruthy();
|
||||
} else {
|
||||
// Some servers might not timeout quickly
|
||||
console.log('Server did not timeout connection (may have long timeout setting)');
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-11: Keep-alive during long operations', async () => {
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
keepAlive: true,
|
||||
keepAliveInterval: 2000,
|
||||
connectionTimeout: 10000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Simulate a long operation
|
||||
console.log('Starting simulated long operation...');
|
||||
|
||||
// Send initial MAIL FROM
|
||||
await smtpClient.sendCommand('MAIL FROM:<sender@example.com>');
|
||||
|
||||
// Track keep-alives during operation
|
||||
let keepAliveDuringOperation = 0;
|
||||
|
||||
smtpClient.on('keepalive', () => {
|
||||
keepAliveDuringOperation++;
|
||||
});
|
||||
|
||||
// Simulate processing delay
|
||||
await new Promise(resolve => setTimeout(resolve, 5000));
|
||||
|
||||
// Continue with RCPT TO
|
||||
await smtpClient.sendCommand('RCPT TO:<recipient@example.com>');
|
||||
|
||||
// More delay
|
||||
await new Promise(resolve => setTimeout(resolve, 3000));
|
||||
|
||||
// Should have sent keep-alives during delays
|
||||
expect(keepAliveDuringOperation).toBeGreaterThan(0);
|
||||
console.log(`Sent ${keepAliveDuringOperation} keep-alives during operation`);
|
||||
|
||||
// Reset the session
|
||||
await smtpClient.sendCommand('RSET');
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('CCM-11: Keep-alive interval adjustment', async () => {
|
||||
const intervals = [1000, 3000, 5000]; // Different intervals to test
|
||||
|
||||
for (const interval of intervals) {
|
||||
console.log(`\nTesting keep-alive with ${interval}ms interval`);
|
||||
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
keepAlive: true,
|
||||
keepAliveInterval: interval,
|
||||
connectionTimeout: 10000,
|
||||
debug: false // Less verbose for this test
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
let keepAliveCount = 0;
|
||||
let keepAliveTimes: number[] = [];
|
||||
let lastTime = Date.now();
|
||||
|
||||
smtpClient.on('keepalive', () => {
|
||||
const now = Date.now();
|
||||
const elapsed = now - lastTime;
|
||||
keepAliveTimes.push(elapsed);
|
||||
lastTime = now;
|
||||
keepAliveCount++;
|
||||
});
|
||||
|
||||
// Wait for multiple intervals
|
||||
await new Promise(resolve => setTimeout(resolve, interval * 3.5));
|
||||
|
||||
// Should have sent approximately 3 keep-alives
|
||||
expect(keepAliveCount).toBeGreaterThanOrEqual(2);
|
||||
expect(keepAliveCount).toBeLessThanOrEqual(4);
|
||||
|
||||
// Check interval accuracy (allowing 20% variance)
|
||||
const avgInterval = keepAliveTimes.reduce((a, b) => a + b, 0) / keepAliveTimes.length;
|
||||
expect(avgInterval).toBeGreaterThan(interval * 0.8);
|
||||
expect(avgInterval).toBeLessThan(interval * 1.2);
|
||||
|
||||
console.log(`Sent ${keepAliveCount} keep-alives, avg interval: ${avgInterval.toFixed(0)}ms`);
|
||||
|
||||
await smtpClient.close();
|
||||
}
|
||||
});
|
||||
|
||||
tap.test('CCM-11: TCP keep-alive socket options', async () => {
|
||||
// Test low-level TCP keep-alive options
|
||||
const smtpClient = createSmtpClient({
|
||||
host: testServer.hostname,
|
||||
port: testServer.port,
|
||||
secure: false,
|
||||
socketOptions: {
|
||||
keepAlive: true,
|
||||
keepAliveInitialDelay: 1000
|
||||
},
|
||||
connectionTimeout: 10000,
|
||||
debug: true
|
||||
});
|
||||
|
||||
let socketConfigured = false;
|
||||
|
||||
smtpClient.on('connection', (info: any) => {
|
||||
if (info && info.socket && info.socket instanceof net.Socket) {
|
||||
// Check if keep-alive is enabled at socket level
|
||||
const socket = info.socket as net.Socket;
|
||||
|
||||
// These methods might not be available in all Node versions
|
||||
if (typeof socket.setKeepAlive === 'function') {
|
||||
socket.setKeepAlive(true, 1000);
|
||||
socketConfigured = true;
|
||||
console.log('TCP keep-alive configured at socket level');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
await smtpClient.connect();
|
||||
|
||||
// Wait a bit to ensure socket options take effect
|
||||
await new Promise(resolve => setTimeout(resolve, 2000));
|
||||
|
||||
expect(smtpClient.isConnected()).toBeTruthy();
|
||||
|
||||
if (!socketConfigured) {
|
||||
console.log('Socket-level keep-alive configuration not available');
|
||||
}
|
||||
|
||||
await smtpClient.close();
|
||||
});
|
||||
|
||||
tap.test('cleanup test SMTP server', async () => {
|
||||
if (testServer) {
|
||||
await testServer.stop();
|
||||
}
|
||||
});
|
||||
|
||||
export default tap.start();
|
Reference in New Issue
Block a user