dcrouter/test/test.dns-validation.ts
Philipp Kunz 37e1ecefd2 refactor(dns): extend DnsValidator to DnsManager with DNS record creation
- 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
2025-05-30 08:52:07 +00:00

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();