dcrouter/test/helpers/server.loader.ts
Philipp Kunz a2b413a78f fix(test): repair SMTP test suite after rate limiter integration
The test helper's mock email server was missing the getRateLimiter() method
that was added during the rate limiting feature implementation. This caused
all SMTP tests to fail with "getRateLimiter is not a function" error.

Changes:
- Add getRateLimiter() method to mock email server that returns a mock rate limiter
- Update mock rate limiter method signatures to match actual implementation
- Fix TypeScript type issue with auth options by adding explicit casting
2025-05-30 16:17:02 +00:00

347 lines
10 KiB
TypeScript

import * as plugins from '../../ts/plugins.js';
import { UnifiedEmailServer } from '../../ts/mail/routing/classes.unified.email.server.js';
import { createSmtpServer } from '../../ts/mail/delivery/smtpserver/index.js';
import type { ISmtpServerOptions } from '../../ts/mail/delivery/smtpserver/interfaces.js';
import type { net } from '../../ts/plugins.js';
export interface ITestServerConfig {
port: number;
hostname?: string;
tlsEnabled?: boolean;
authRequired?: boolean;
timeout?: number;
testCertPath?: string;
testKeyPath?: string;
maxConnections?: number;
size?: number;
maxRecipients?: number;
}
export interface ITestServer {
server: any;
smtpServer: any;
port: number;
hostname: string;
config: ITestServerConfig;
startTime: number;
}
/**
* Starts a test SMTP server with the given configuration
*/
export async function startTestServer(config: ITestServerConfig): Promise<ITestServer> {
const serverConfig = {
port: config.port || 2525,
hostname: config.hostname || 'localhost',
tlsEnabled: config.tlsEnabled || false,
authRequired: config.authRequired || false,
timeout: config.timeout || 30000,
maxConnections: config.maxConnections || 100,
size: config.size || 10 * 1024 * 1024, // 10MB default
maxRecipients: config.maxRecipients || 100
};
// Create a mock email server for testing
const mockEmailServer = {
processEmailByMode: async (emailData: any) => {
console.log(`📧 [Test Server] Processing email:`, emailData.subject || 'No subject');
return emailData;
},
getRateLimiter: () => {
// Return a mock rate limiter for testing
return {
recordConnection: (_ip: string) => ({ allowed: true, remaining: 100 }),
checkConnectionLimit: async (_ip: string) => ({ allowed: true, remaining: 100 }),
checkMessageLimit: (_senderAddress: string, _ip: string, _recipientCount?: number, _pattern?: string, _domain?: string) => ({ allowed: true, remaining: 1000 }),
checkRecipientLimit: async (_session: any) => ({ allowed: true, remaining: 50 }),
recordAuthenticationFailure: async (_ip: string) => {},
recordSyntaxError: async (_ip: string) => {},
recordCommandError: async (_ip: string) => {},
isBlocked: async (_ip: string) => false,
cleanup: async () => {}
};
}
} as any;
// Load test certificates
let key: string;
let cert: string;
if (serverConfig.tlsEnabled) {
try {
const certPath = config.testCertPath || './test/fixtures/test-cert.pem';
const keyPath = config.testKeyPath || './test/fixtures/test-key.pem';
cert = await plugins.fs.promises.readFile(certPath, 'utf8');
key = await plugins.fs.promises.readFile(keyPath, 'utf8');
} catch (error) {
console.warn('⚠️ Failed to load TLS certificates, falling back to self-signed');
// Generate self-signed certificate for testing
const forge = await import('node-forge');
const pki = forge.default.pki;
// Generate key pair
const keys = pki.rsa.generateKeyPair(2048);
// Create certificate
const certificate = pki.createCertificate();
certificate.publicKey = keys.publicKey;
certificate.serialNumber = '01';
certificate.validity.notBefore = new Date();
certificate.validity.notAfter = new Date();
certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1);
const attrs = [{
name: 'commonName',
value: serverConfig.hostname
}];
certificate.setSubject(attrs);
certificate.setIssuer(attrs);
certificate.sign(keys.privateKey);
// Convert to PEM
cert = pki.certificateToPem(certificate);
key = pki.privateKeyToPem(keys.privateKey);
}
} else {
// Always provide a self-signed certificate for non-TLS servers
// This is required by the interface
const forge = await import('node-forge');
const pki = forge.default.pki;
// Generate key pair
const keys = pki.rsa.generateKeyPair(2048);
// Create certificate
const certificate = pki.createCertificate();
certificate.publicKey = keys.publicKey;
certificate.serialNumber = '01';
certificate.validity.notBefore = new Date();
certificate.validity.notAfter = new Date();
certificate.validity.notAfter.setFullYear(certificate.validity.notBefore.getFullYear() + 1);
const attrs = [{
name: 'commonName',
value: serverConfig.hostname
}];
certificate.setSubject(attrs);
certificate.setIssuer(attrs);
certificate.sign(keys.privateKey);
// Convert to PEM
cert = pki.certificateToPem(certificate);
key = pki.privateKeyToPem(keys.privateKey);
}
// SMTP server options
const smtpOptions: ISmtpServerOptions = {
port: serverConfig.port,
hostname: serverConfig.hostname,
key: key,
cert: cert,
maxConnections: serverConfig.maxConnections,
size: serverConfig.size,
maxRecipients: serverConfig.maxRecipients,
socketTimeout: serverConfig.timeout,
connectionTimeout: serverConfig.timeout * 2,
cleanupInterval: 300000,
auth: serverConfig.authRequired ? ({
required: true,
methods: ['PLAIN', 'LOGIN'] as ('PLAIN' | 'LOGIN' | 'OAUTH2')[],
validateUser: async (username: string, password: string) => {
// Test server accepts these credentials
return username === 'testuser' && password === 'testpass';
}
} as any) : undefined
};
// Create SMTP server
const smtpServer = await createSmtpServer(mockEmailServer, smtpOptions);
// Start the server
await smtpServer.listen();
// Wait for server to be ready
await waitForServerReady(serverConfig.hostname, serverConfig.port);
console.log(`✅ Test SMTP server started on ${serverConfig.hostname}:${serverConfig.port}`);
return {
server: mockEmailServer,
smtpServer: smtpServer,
port: serverConfig.port,
hostname: serverConfig.hostname,
config: serverConfig,
startTime: Date.now()
};
}
/**
* Stops a test SMTP server
*/
export async function stopTestServer(testServer: ITestServer): Promise<void> {
if (!testServer || !testServer.smtpServer) {
console.warn('⚠️ No test server to stop');
return;
}
try {
console.log(`🛑 Stopping test SMTP server on ${testServer.hostname}:${testServer.port}`);
// Stop the SMTP server
if (testServer.smtpServer.close && typeof testServer.smtpServer.close === 'function') {
await testServer.smtpServer.close();
}
// Wait for port to be free
await waitForPortFree(testServer.port);
console.log(`✅ Test SMTP server stopped`);
} catch (error) {
console.error('❌ Error stopping test server:', error);
throw error;
}
}
/**
* Wait for server to be ready to accept connections
*/
async function waitForServerReady(hostname: string, port: number, timeout: number = 10000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
try {
await new Promise<void>((resolve, reject) => {
const socket = plugins.net.createConnection({ port, host: hostname });
socket.on('connect', () => {
socket.end();
resolve();
});
socket.on('error', reject);
setTimeout(() => {
socket.destroy();
reject(new Error('Connection timeout'));
}, 1000);
});
return; // Server is ready
} catch {
// Server not ready yet, wait and retry
await new Promise(resolve => setTimeout(resolve, 100));
}
}
throw new Error(`Server did not become ready within ${timeout}ms`);
}
/**
* Wait for port to be free
*/
async function waitForPortFree(port: number, timeout: number = 5000): Promise<void> {
const startTime = Date.now();
while (Date.now() - startTime < timeout) {
const isFree = await isPortFree(port);
if (isFree) {
return;
}
await new Promise(resolve => setTimeout(resolve, 100));
}
console.warn(`⚠️ Port ${port} still in use after ${timeout}ms`);
}
/**
* Check if a port is free
*/
async function isPortFree(port: number): Promise<boolean> {
return new Promise((resolve) => {
const server = plugins.net.createServer();
server.listen(port, () => {
server.close(() => resolve(true));
});
server.on('error', () => resolve(false));
});
}
/**
* Get an available port for testing
*/
export async function getAvailablePort(startPort: number = 25000): Promise<number> {
for (let port = startPort; port < startPort + 1000; port++) {
if (await isPortFree(port)) {
return port;
}
}
throw new Error(`No available ports found starting from ${startPort}`);
}
/**
* Create test email data
*/
export function createTestEmail(options: {
from?: string;
to?: string | string[];
subject?: string;
text?: string;
html?: string;
attachments?: any[];
} = {}): any {
return {
from: options.from || 'test@example.com',
to: options.to || 'recipient@example.com',
subject: options.subject || 'Test Email',
text: options.text || 'This is a test email',
html: options.html || '<p>This is a test email</p>',
attachments: options.attachments || [],
date: new Date(),
messageId: `<${Date.now()}@test.example.com>`
};
}
/**
* Simple test server for custom protocol testing
*/
export interface ISimpleTestServer {
server: any;
hostname: string;
port: number;
}
export async function createTestServer(options: {
onConnection?: (socket: any) => void | Promise<void>;
port?: number;
hostname?: string;
}): Promise<ISimpleTestServer> {
const hostname = options.hostname || 'localhost';
const port = options.port || await getAvailablePort();
const server = plugins.net.createServer((socket) => {
if (options.onConnection) {
const result = options.onConnection(socket);
if (result && typeof result.then === 'function') {
result.catch(error => {
console.error('Error in onConnection handler:', error);
socket.destroy();
});
}
}
});
return new Promise((resolve, reject) => {
server.listen(port, hostname, () => {
resolve({
server,
hostname,
port
});
});
server.on('error', reject);
});
}