- Rename DnsValidator to DnsManager to better reflect its expanded responsibilities - Move DNS record creation logic from UnifiedEmailServer to DnsManager - Add ensureDnsRecords() method that handles both validation and creation - Consolidate internal DNS record creation (MX, SPF, DMARC) in one place - Keep DKIM key generation in UnifiedEmailServer but move DNS registration to DnsManager - Update all imports and tests to use DnsManager instead of DnsValidator - Improve code organization and discoverability of DNS functionality
283 lines
9.1 KiB
TypeScript
283 lines
9.1 KiB
TypeScript
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
|
import * as plugins from '../ts/plugins.js';
|
|
import * as paths from '../ts/paths.js';
|
|
import { DnsManager } from '../ts/mail/routing/classes.dns.manager.js';
|
|
import { DomainRegistry } from '../ts/mail/routing/classes.domain.registry.js';
|
|
import { StorageManager } from '../ts/storage/classes.storagemanager.js';
|
|
import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.js';
|
|
import type { IEmailDomainConfig } from '../ts/mail/routing/interfaces.js';
|
|
|
|
// Mock DcRouter for testing
|
|
class MockDcRouter {
|
|
public storageManager: StorageManager;
|
|
public options: any;
|
|
|
|
constructor(testDir: string, dnsDomain?: string) {
|
|
this.storageManager = new StorageManager({ fsPath: testDir });
|
|
this.options = {
|
|
dnsDomain
|
|
};
|
|
}
|
|
}
|
|
|
|
// Mock DNS resolver for testing
|
|
class MockDnsManager extends DnsManager {
|
|
private mockNsRecords: Map<string, string[]> = new Map();
|
|
private mockTxtRecords: Map<string, string[][]> = new Map();
|
|
private mockMxRecords: Map<string, any[]> = new Map();
|
|
|
|
setNsRecords(domain: string, records: string[]) {
|
|
this.mockNsRecords.set(domain, records);
|
|
}
|
|
|
|
setTxtRecords(domain: string, records: string[][]) {
|
|
this.mockTxtRecords.set(domain, records);
|
|
}
|
|
|
|
setMxRecords(domain: string, records: any[]) {
|
|
this.mockMxRecords.set(domain, records);
|
|
}
|
|
|
|
protected async resolveNs(domain: string): Promise<string[]> {
|
|
return this.mockNsRecords.get(domain) || [];
|
|
}
|
|
|
|
protected async resolveTxt(domain: string): Promise<string[][]> {
|
|
return this.mockTxtRecords.get(domain) || [];
|
|
}
|
|
|
|
protected async resolveMx(domain: string): Promise<any[]> {
|
|
return this.mockMxRecords.get(domain) || [];
|
|
}
|
|
}
|
|
|
|
tap.test('DNS Validator - Forward Mode', async () => {
|
|
const testDir = plugins.path.join(paths.dataDir, '.test-dns-forward');
|
|
const mockRouter = new MockDcRouter(testDir) as any;
|
|
const validator = new DnsManager(mockRouter);
|
|
|
|
const config: IEmailDomainConfig = {
|
|
domain: 'forward.example.com',
|
|
dnsMode: 'forward',
|
|
dns: {
|
|
forward: {
|
|
skipDnsValidation: true
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateDomain(config);
|
|
|
|
expect(result.valid).toEqual(true);
|
|
expect(result.errors.length).toEqual(0);
|
|
expect(result.warnings.length).toBeGreaterThan(0); // Should have warning about forward mode
|
|
|
|
// Clean up
|
|
await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
tap.test('DNS Validator - Internal DNS Mode', async () => {
|
|
const testDir = plugins.path.join(paths.dataDir, '.test-dns-internal');
|
|
const mockRouter = new MockDcRouter(testDir, 'ns.myservice.com') as any;
|
|
const validator = new MockDnsManager(mockRouter);
|
|
|
|
// Setup NS delegation
|
|
validator.setNsRecords('mail.example.com', ['ns.myservice.com']);
|
|
|
|
const config: IEmailDomainConfig = {
|
|
domain: 'mail.example.com',
|
|
dnsMode: 'internal-dns',
|
|
dns: {
|
|
internal: {
|
|
mxPriority: 10,
|
|
ttl: 3600
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateDomain(config);
|
|
|
|
expect(result.valid).toEqual(true);
|
|
expect(result.errors.length).toEqual(0);
|
|
|
|
// Test without NS delegation
|
|
validator.setNsRecords('mail2.example.com', ['other.nameserver.com']);
|
|
|
|
const config2: IEmailDomainConfig = {
|
|
domain: 'mail2.example.com',
|
|
dnsMode: 'internal-dns'
|
|
};
|
|
|
|
const result2 = await validator.validateDomain(config2);
|
|
|
|
// Should have warnings but still be valid (warnings don't make it invalid)
|
|
expect(result2.valid).toEqual(true);
|
|
expect(result2.warnings.length).toBeGreaterThan(0);
|
|
expect(result2.requiredChanges.length).toBeGreaterThan(0);
|
|
|
|
// Clean up
|
|
await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
tap.test('DNS Validator - External DNS Mode', async () => {
|
|
const testDir = plugins.path.join(paths.dataDir, '.test-dns-external');
|
|
const mockRouter = new MockDcRouter(testDir) as any;
|
|
const validator = new MockDnsManager(mockRouter);
|
|
|
|
// Setup mock DNS records
|
|
validator.setMxRecords('example.com', [
|
|
{ priority: 10, exchange: 'mail.example.com' }
|
|
]);
|
|
validator.setTxtRecords('example.com', [
|
|
['v=spf1 mx ~all']
|
|
]);
|
|
validator.setTxtRecords('default._domainkey.example.com', [
|
|
['v=DKIM1; k=rsa; p=MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQ...']
|
|
]);
|
|
validator.setTxtRecords('_dmarc.example.com', [
|
|
['v=DMARC1; p=none; rua=mailto:dmarc@example.com']
|
|
]);
|
|
|
|
const config: IEmailDomainConfig = {
|
|
domain: 'example.com',
|
|
dnsMode: 'external-dns',
|
|
dns: {
|
|
external: {
|
|
requiredRecords: ['MX', 'SPF', 'DKIM', 'DMARC']
|
|
}
|
|
}
|
|
};
|
|
|
|
const result = await validator.validateDomain(config);
|
|
|
|
// External DNS validation checks if records exist and provides instructions
|
|
expect(result.valid).toEqual(true);
|
|
expect(result.errors.length).toEqual(0);
|
|
|
|
// Clean up
|
|
await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
tap.test('DKIM Key Generation', async () => {
|
|
const testDir = plugins.path.join(paths.dataDir, '.test-dkim-generation');
|
|
const storage = new StorageManager({ fsPath: testDir });
|
|
|
|
// Ensure keys directory exists
|
|
const keysDir = plugins.path.join(testDir, 'keys');
|
|
await plugins.fs.promises.mkdir(keysDir, { recursive: true });
|
|
|
|
const dkimCreator = new DKIMCreator(keysDir, storage);
|
|
|
|
// Generate DKIM keys
|
|
await dkimCreator.handleDKIMKeysForDomain('test.example.com');
|
|
|
|
// Verify keys were created
|
|
const keys = await dkimCreator.readDKIMKeys('test.example.com');
|
|
expect(keys.privateKey).toBeTruthy();
|
|
expect(keys.publicKey).toBeTruthy();
|
|
expect(keys.privateKey).toContain('BEGIN RSA PRIVATE KEY');
|
|
expect(keys.publicKey).toContain('BEGIN PUBLIC KEY');
|
|
|
|
// Get DNS record
|
|
const dnsRecord = await dkimCreator.getDNSRecordForDomain('test.example.com');
|
|
expect(dnsRecord.name).toEqual('mta._domainkey.test.example.com');
|
|
expect(dnsRecord.type).toEqual('TXT');
|
|
expect(dnsRecord.value).toContain('v=DKIM1');
|
|
expect(dnsRecord.value).toContain('k=rsa');
|
|
expect(dnsRecord.value).toContain('p=');
|
|
|
|
// Test key rotation
|
|
const needsRotation = await dkimCreator.needsRotation('test.example.com', 'default', 0); // 0 days = always rotate
|
|
expect(needsRotation).toEqual(true);
|
|
|
|
// Clean up
|
|
await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
tap.test('Domain Registry', async () => {
|
|
// Test domain configurations
|
|
const domains: IEmailDomainConfig[] = [
|
|
{
|
|
domain: 'simple.example.com',
|
|
dnsMode: 'internal-dns'
|
|
},
|
|
{
|
|
domain: 'configured.example.com',
|
|
dnsMode: 'external-dns',
|
|
dkim: {
|
|
selector: 'custom',
|
|
keySize: 4096
|
|
},
|
|
rateLimits: {
|
|
outbound: {
|
|
messagesPerMinute: 100
|
|
}
|
|
}
|
|
}
|
|
];
|
|
|
|
const defaults = {
|
|
dnsMode: 'internal-dns' as const,
|
|
dkim: {
|
|
selector: 'default',
|
|
keySize: 2048
|
|
}
|
|
};
|
|
|
|
const registry = new DomainRegistry(domains, defaults);
|
|
|
|
// Test simple domain (uses defaults)
|
|
const simpleConfig = registry.getDomainConfig('simple.example.com');
|
|
expect(simpleConfig).toBeTruthy();
|
|
expect(simpleConfig?.dnsMode).toEqual('internal-dns');
|
|
expect(simpleConfig?.dkim?.selector).toEqual('default');
|
|
expect(simpleConfig?.dkim?.keySize).toEqual(2048);
|
|
|
|
// Test configured domain
|
|
const configuredConfig = registry.getDomainConfig('configured.example.com');
|
|
expect(configuredConfig).toBeTruthy();
|
|
expect(configuredConfig?.dnsMode).toEqual('external-dns');
|
|
expect(configuredConfig?.dkim?.selector).toEqual('custom');
|
|
expect(configuredConfig?.dkim?.keySize).toEqual(4096);
|
|
expect(configuredConfig?.rateLimits?.outbound?.messagesPerMinute).toEqual(100);
|
|
|
|
// Test non-existent domain
|
|
const nonExistent = registry.getDomainConfig('nonexistent.example.com');
|
|
expect(nonExistent).toEqual(undefined); // Returns undefined, not null
|
|
|
|
// Test getting all domains
|
|
const allDomains = registry.getAllDomains();
|
|
expect(allDomains.length).toEqual(2);
|
|
expect(allDomains).toContain('simple.example.com');
|
|
expect(allDomains).toContain('configured.example.com');
|
|
});
|
|
|
|
tap.test('DNS Record Generation', async () => {
|
|
const testDir = plugins.path.join(paths.dataDir, '.test-dns-records');
|
|
const storage = new StorageManager({ fsPath: testDir });
|
|
|
|
// Ensure keys directory exists
|
|
const keysDir = plugins.path.join(testDir, 'keys');
|
|
await plugins.fs.promises.mkdir(keysDir, { recursive: true });
|
|
|
|
const dkimCreator = new DKIMCreator(keysDir, storage);
|
|
|
|
// Generate DKIM keys first
|
|
await dkimCreator.handleDKIMKeysForDomain('records.example.com');
|
|
|
|
// Test DNS record for domain
|
|
const dkimRecord = await dkimCreator.getDNSRecordForDomain('records.example.com');
|
|
|
|
// Check DKIM record
|
|
expect(dkimRecord).toBeTruthy();
|
|
expect(dkimRecord.name).toContain('_domainkey.records.example.com');
|
|
expect(dkimRecord.value).toContain('v=DKIM1');
|
|
|
|
// Note: The DnsManager doesn't have a generateDnsRecords method exposed
|
|
// DNS records are handled internally or by the DNS server component
|
|
|
|
// Clean up
|
|
await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {});
|
|
});
|
|
|
|
export default tap.start(); |