feat(storage): implement StorageManager with filesystem support and component integration
- Add StorageManager with filesystem, custom, and memory backends - Update DKIMCreator and BounceManager to use StorageManager - Remove component-level storage warnings (handled by StorageManager) - Fix list() method for filesystem backend - Add comprehensive storage and integration tests - Implement DNS mode switching tests - Complete Phase 4 testing tasks from plan
This commit is contained in:
283
test/test.dns-validation.ts
Normal file
283
test/test.dns-validation.ts
Normal file
@ -0,0 +1,283 @@
|
||||
import { tap, expect } from '@git.zone/tstest/tapbundle';
|
||||
import * as plugins from '../ts/plugins.js';
|
||||
import * as paths from '../ts/paths.js';
|
||||
import { DnsValidator } from '../ts/mail/routing/classes.dns.validator.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 MockDnsValidator extends DnsValidator {
|
||||
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 DnsValidator(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 MockDnsValidator(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 MockDnsValidator(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 DnsValidator 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();
|
Reference in New Issue
Block a user