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:
118
test/test.e2e.server-lifecycle.node.ts
Normal file
118
test/test.e2e.server-lifecycle.node.ts
Normal 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();
|
||||
Reference in New Issue
Block a user