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
347 lines
10 KiB
TypeScript
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);
|
|
});
|
|
} |