Files
dcrouter/test/test.dcrouter.email.ts
T

250 lines
6.9 KiB
TypeScript

import { tap, expect } from '@git.zone/tstest/tapbundle';
import * as plugins from '../ts/plugins.js';
import * as path from 'path';
import * as fs from 'fs';
import * as net from 'node:net';
import { DcRouter, type IDcRouterOptions } from '../ts/classes.dcrouter.js';
import type { IUnifiedEmailServerOptions } from '@push.rocks/smartmta';
async function getFreePort(): Promise<number> {
return await new Promise<number>((resolve, reject) => {
const server = net.createServer();
server.once('error', reject);
server.listen(0, '127.0.0.1', () => {
const address = server.address();
const port = typeof address === 'object' && address ? address.port : 0;
server.close(() => resolve(port));
});
});
}
tap.test('DcRouter class - Custom email port configuration', async () => {
// Define custom port mapping
const customPortMapping: Record<number, number> = {
25: 11025, // Custom SMTP port mapping
587: 11587, // Custom submission port mapping
465: 11465, // Custom SMTPS port mapping
2525: 12525 // Additional custom port
};
// Create a custom email configuration using smartmta interfaces
const emailConfig: IUnifiedEmailServerOptions = {
ports: [25, 587, 465, 2525],
hostname: 'mail.example.com',
maxMessageSize: 50 * 1024 * 1024, // 50MB
domains: [
{
domain: 'example.com',
dnsMode: 'external-dns',
},
{
domain: 'example.org',
dnsMode: 'external-dns',
}
],
routes: [
{
name: 'forward-example-com',
match: {
recipients: '*@example.com',
},
action: {
type: 'forward',
forward: {
host: 'mail1.example.com',
port: 25,
}
}
},
{
name: 'deliver-example-org',
match: {
recipients: '*@example.org',
},
action: {
type: 'deliver',
process: {
dkim: true,
}
}
}
]
};
// Create DcRouter options with custom email port configuration
const options: IDcRouterOptions = {
emailConfig,
emailPortConfig: {
portMapping: customPortMapping,
portSettings: {
2525: {
terminateTls: false,
routeName: 'custom-smtp-route'
}
},
},
tls: {
contactEmail: 'test@example.com'
}
};
// Create DcRouter instance
const router = new DcRouter(options);
// Verify the options are correctly set
expect(router.options.emailPortConfig).toBeTruthy();
expect(router.options.emailPortConfig!.portMapping).toEqual(customPortMapping);
// Test the generateEmailRoutes method
if (typeof (router as any)['generateEmailRoutes'] === 'function') {
const routes = (router as any)['generateEmailRoutes'](emailConfig);
// Verify that all ports are configured
expect(routes.length).toBeGreaterThan(0);
// Check the custom port configuration
const customPortRoute = routes.find((r: any) => {
const ports = r.match.ports;
return ports === 2525 || (Array.isArray(ports) && (ports as number[]).includes(2525));
});
expect(customPortRoute).toBeTruthy();
expect(customPortRoute?.name).toEqual('custom-smtp-route');
expect(customPortRoute?.action.targets[0].port).toEqual(12525);
expect(customPortRoute?.remoteIngress).toBeUndefined();
// Check standard port mappings
const smtpRoute = routes.find((r: any) => {
const ports = r.match.ports;
return ports === 25 || (Array.isArray(ports) && (ports as number[]).includes(25));
});
expect(smtpRoute?.action.targets[0].port).toEqual(11025);
const submissionRoute = routes.find((r: any) => {
const ports = r.match.ports;
return ports === 587 || (Array.isArray(ports) && (ports as number[]).includes(587));
});
expect(submissionRoute?.action.targets[0].port).toEqual(11587);
}
});
tap.test('DcRouter class - Email routes are exposed through RemoteIngress when enabled', async () => {
const emailConfig: IUnifiedEmailServerOptions = {
ports: [25, 587, 465],
hostname: 'mail.example.com',
domains: [],
routes: [],
};
const router = new DcRouter({
emailConfig,
remoteIngressConfig: {
enabled: true,
tunnelPort: 8443,
hubDomain: 'ingress.example.com',
},
});
const routes = (router as any)['generateEmailRoutes'](emailConfig);
expect(routes.length).toEqual(3);
for (const route of routes) {
expect(route.remoteIngress).toEqual({ enabled: true });
}
});
tap.test('DcRouter class - Email config with domains and routes', async () => {
const opsServerPort = await getFreePort();
// Create a basic email configuration
const emailConfig: IUnifiedEmailServerOptions = {
ports: [2525],
hostname: 'mail.example.com',
domains: [],
routes: []
};
// Create DcRouter options
const options: IDcRouterOptions = {
emailConfig,
tls: {
contactEmail: 'test@example.com'
},
opsServerPort,
dbConfig: {
enabled: false,
}
};
// Create DcRouter instance
const router = new DcRouter(options);
// Start the router to initialize email services
await router.start();
// Verify unified email server was initialized
expect(router.emailServer).toBeTruthy();
expect((router.emailServer as any).options.hostname).toEqual('mail.example.com');
expect((router.emailServer as any).options.persistRoutes).toEqual(false);
expect((router.emailServer as any).options.queue.storageType).toEqual('disk');
// Stop the router
await router.stop();
});
tap.test('DcRouter class - Email config updates are serialized', async () => {
const router = new DcRouter({
tls: {
contactEmail: 'test@example.com',
},
});
const delay = async () => await new Promise<void>((resolve) => setTimeout(resolve, 10));
let activeLifecycleSteps = 0;
let overlapped = false;
const enterLifecycleStep = async () => {
activeLifecycleSteps++;
if (activeLifecycleSteps > 1) {
overlapped = true;
}
await delay();
activeLifecycleSteps--;
};
(router as any).stopUnifiedEmailComponents = async () => {
await enterLifecycleStep();
};
(router as any).setupUnifiedEmailHandling = async () => {
await enterLifecycleStep();
};
const firstConfig: IUnifiedEmailServerOptions = {
ports: [2525],
hostname: 'first.mail.example.com',
domains: [],
routes: [],
};
const secondConfig: IUnifiedEmailServerOptions = {
ports: [2526],
hostname: 'second.mail.example.com',
domains: [],
routes: [],
};
await Promise.all([
router.updateEmailConfig(firstConfig),
router.updateEmailConfig(secondConfig),
]);
expect(overlapped).toEqual(false);
expect(router.options.emailConfig?.hostname).toEqual('second.mail.example.com');
});
// Final clean-up test
tap.test('clean up after tests', async () => {
// No-op
});
tap.test('stop', async () => {
await tap.stopForcefully();
});
export default tap.start();