feat(e2e-tests): add Node.js end-to-end tests covering server lifecycle, inbound SMTP handling, outbound delivery and routing actions

This commit is contained in:
2026-02-11 07:31:08 +00:00
parent a7ea1d86cb
commit f7af8c4534
6 changed files with 801 additions and 1 deletions

View File

@@ -0,0 +1,118 @@
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();