119 lines
3.4 KiB
TypeScript
119 lines
3.4 KiB
TypeScript
|
|
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||
|
|
import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js';
|
||
|
|
import { RustSecurityBridge, BridgeState } from '../ts/security/classes.rustsecuritybridge.js';
|
||
|
|
import { connectToSmtp, waitForGreeting } from './helpers/utils.js';
|
||
|
|
import * as net from 'net';
|
||
|
|
|
||
|
|
// Common mock pattern for dcRouter dependency
|
||
|
|
const storageMap = new Map<string, string>();
|
||
|
|
const mockDcRouter = {
|
||
|
|
storageManager: {
|
||
|
|
get: async (key: string) => storageMap.get(key) || null,
|
||
|
|
set: async (key: string, value: string) => { storageMap.set(key, value); },
|
||
|
|
},
|
||
|
|
};
|
||
|
|
|
||
|
|
let server: UnifiedEmailServer;
|
||
|
|
let bridge: RustSecurityBridge;
|
||
|
|
|
||
|
|
tap.test('setup - reset bridge singleton', async () => {
|
||
|
|
RustSecurityBridge.resetInstance();
|
||
|
|
bridge = RustSecurityBridge.getInstance();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('construct server - should create UnifiedEmailServer', async () => {
|
||
|
|
server = new UnifiedEmailServer(mockDcRouter, {
|
||
|
|
ports: [10025, 10587],
|
||
|
|
hostname: 'test.e2e.local',
|
||
|
|
domains: [
|
||
|
|
{
|
||
|
|
domain: 'e2e-test.com',
|
||
|
|
dnsMode: 'forward',
|
||
|
|
},
|
||
|
|
],
|
||
|
|
routes: [
|
||
|
|
{
|
||
|
|
name: 'catch-all',
|
||
|
|
priority: 0,
|
||
|
|
match: {
|
||
|
|
recipients: '*@e2e-test.com',
|
||
|
|
},
|
||
|
|
action: {
|
||
|
|
type: 'process',
|
||
|
|
},
|
||
|
|
},
|
||
|
|
],
|
||
|
|
});
|
||
|
|
expect(server).toBeTruthy();
|
||
|
|
expect(server).toBeInstanceOf(UnifiedEmailServer);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('start server - should start and accept SMTP connections', async () => {
|
||
|
|
try {
|
||
|
|
await server.start();
|
||
|
|
} catch (err) {
|
||
|
|
console.log(`SKIP: Server failed to start — ${(err as Error).message}`);
|
||
|
|
console.log('Build the Rust binary with: cd rust && cargo build --release');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(bridge.running).toBeTrue();
|
||
|
|
|
||
|
|
// Connect to port 10025 and verify we get a 220 greeting
|
||
|
|
const socket = await connectToSmtp('127.0.0.1', 10025, 10000);
|
||
|
|
const greeting = await waitForGreeting(socket, 10000);
|
||
|
|
expect(greeting).toInclude('220');
|
||
|
|
socket.destroy();
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('get stats - should return server statistics', async () => {
|
||
|
|
if (!bridge.running) {
|
||
|
|
console.log('SKIP: bridge not running');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
const stats = server.getStats();
|
||
|
|
expect(stats).toBeTruthy();
|
||
|
|
expect(stats.startTime).toBeInstanceOf(Date);
|
||
|
|
expect(stats.connections).toBeTruthy();
|
||
|
|
expect(typeof stats.connections.current).toEqual('number');
|
||
|
|
expect(typeof stats.connections.total).toEqual('number');
|
||
|
|
expect(stats.messages).toBeTruthy();
|
||
|
|
expect(typeof stats.messages.processed).toEqual('number');
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('stop server - should stop and refuse connections', async () => {
|
||
|
|
if (!bridge.running) {
|
||
|
|
console.log('SKIP: bridge not running');
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
await server.stop();
|
||
|
|
|
||
|
|
// Verify connection is refused after stop
|
||
|
|
try {
|
||
|
|
const socket = await connectToSmtp('127.0.0.1', 10025, 3000);
|
||
|
|
socket.destroy();
|
||
|
|
// If we get here, the connection was accepted — that's unexpected
|
||
|
|
throw new Error('Expected connection to be refused after server stop');
|
||
|
|
} catch (err) {
|
||
|
|
// Connection refused or timeout is expected
|
||
|
|
const msg = (err as Error).message;
|
||
|
|
expect(
|
||
|
|
msg.includes('ECONNREFUSED') || msg.includes('timeout') || msg.includes('refused')
|
||
|
|
).toBeTrue();
|
||
|
|
}
|
||
|
|
|
||
|
|
expect(bridge.state).toEqual(BridgeState.Stopped);
|
||
|
|
});
|
||
|
|
|
||
|
|
tap.test('stop', async () => {
|
||
|
|
// Clean up if not already stopped
|
||
|
|
if (bridge.running) {
|
||
|
|
await server.stop();
|
||
|
|
}
|
||
|
|
await tap.stopForcefully();
|
||
|
|
});
|
||
|
|
|
||
|
|
export default tap.start();
|