Files
smartmta/test/test.e2e.routing-actions.node.ts

240 lines
6.2 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import { UnifiedEmailServer } from '../ts/mail/routing/classes.unified.email.server.js';
import { RustSecurityBridge } from '../ts/security/classes.rustsecuritybridge.js';
import { Email } from '../ts/mail/core/classes.email.js';
import { SmtpState } from '../ts/mail/delivery/interfaces.js';
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;
let bridgeAvailable = false;
/**
* Build a minimal SMTP session object for processEmailByMode().
*/
function buildSession(email: Email): any {
return {
id: `test-${Date.now()}-${Math.random().toString(36).substring(2)}`,
state: SmtpState.FINISHED,
mailFrom: email.from,
rcptTo: email.to,
emailData: '',
useTLS: false,
connectionEnded: false,
remoteAddress: '127.0.0.1',
clientHostname: 'test-client.local',
secure: false,
authenticated: false,
envelope: {
mailFrom: { address: email.from, args: {} },
rcptTo: email.to.map((addr: string) => ({ address: addr, args: {} })),
},
};
}
tap.test('setup - start server with routing rules on port 10225', async () => {
RustSecurityBridge.resetInstance();
bridge = RustSecurityBridge.getInstance();
server = new UnifiedEmailServer(mockDcRouter, {
ports: [10225],
hostname: 'test.routing.local',
domains: [
{ domain: 'process.com', dnsMode: 'forward' },
{ domain: 'local.com', dnsMode: 'forward' },
{ domain: 'external.com', dnsMode: 'forward' },
],
routes: [
{
name: 'reject-route',
priority: 40,
match: {
senders: '*@spammer.com',
},
action: {
type: 'reject',
reject: {
code: 550,
message: 'Spam rejected',
},
},
},
{
name: 'process-route',
priority: 30,
match: {
recipients: '*@process.com',
},
action: {
type: 'process',
process: {
scan: true,
},
},
},
{
name: 'deliver-route',
priority: 20,
match: {
recipients: '*@local.com',
},
action: {
type: 'deliver',
},
},
{
name: 'forward-route',
priority: 10,
match: {
recipients: '*@external.com',
},
action: {
type: 'forward',
forward: {
host: '127.0.0.1',
port: 59999, // No server listening — expected failure
},
},
},
],
});
try {
await server.start();
bridgeAvailable = true;
} 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');
}
});
tap.test('process action - queues email for processing', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const email = new Email({
from: 'sender@example.com',
to: 'user@process.com',
subject: 'Process test',
text: 'This email should be queued for processing.',
});
const session = buildSession(email);
const result = await server.processEmailByMode(email, session);
expect(result).toBeTruthy();
const stats = server.deliveryQueue.getStats();
expect(stats.modes.process).toBeGreaterThan(0);
});
tap.test('deliver action - queues email for MTA delivery', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const email = new Email({
from: 'sender@example.com',
to: 'user@local.com',
subject: 'Deliver test',
text: 'This email should be queued for local delivery.',
});
const session = buildSession(email);
const result = await server.processEmailByMode(email, session);
expect(result).toBeTruthy();
const stats = server.deliveryQueue.getStats();
expect(stats.modes.mta).toBeGreaterThan(0);
});
tap.test('reject action - throws with correct code', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const email = new Email({
from: 'bad@spammer.com',
to: 'user@process.com',
subject: 'Spam attempt',
text: 'This should be rejected.',
});
const session = buildSession(email);
try {
await server.processEmailByMode(email, session);
throw new Error('Expected processEmailByMode to throw for rejected email');
} catch (err: any) {
expect(err.responseCode).toEqual(550);
expect(err.message).toInclude('Spam rejected');
}
});
tap.test('forward action - fails to unreachable host', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const email = new Email({
from: 'sender@example.com',
to: 'user@external.com',
subject: 'Forward test',
text: 'This forward should fail — no server at port 59999.',
});
const session = buildSession(email);
try {
await server.processEmailByMode(email, session);
throw new Error('Expected processEmailByMode to throw for unreachable forward host');
} catch (err: any) {
// We expect an error from the failed SMTP connection
expect(err).toBeTruthy();
expect(err.message).toBeTruthy();
}
});
tap.test('no matching route - throws error', async () => {
if (!bridgeAvailable) {
console.log('SKIP: bridge not running');
return;
}
const email = new Email({
from: 'sender@example.com',
to: 'nobody@unmatched.com',
subject: 'Unmatched route test',
text: 'No route matches this recipient.',
});
const session = buildSession(email);
try {
await server.processEmailByMode(email, session);
throw new Error('Expected processEmailByMode to throw for no matching route');
} catch (err: any) {
expect(err.message).toInclude('No matching route');
}
});
tap.test('cleanup - stop server', async () => {
if (bridgeAvailable) {
await server.stop();
}
await tap.stopForcefully();
});
export default tap.start();