diff --git a/test/test.base.ts b/test/test.base.ts deleted file mode 100644 index 2227577..0000000 --- a/test/test.base.ts +++ /dev/null @@ -1,65 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.ts'; -import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.ts'; - -/** - * Basic test to check if our integrated classes work correctly - */ -tap.test('verify that SenderReputationMonitor and IPWarmupManager are functioning', async () => { - // Create instances of both classes - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: true, - domains: ['example.com'] - }); - - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2'], - targetDomains: ['example.com'] - }); - - // Test SenderReputationMonitor - reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 }); - reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 }); - - const reputationData = reputationMonitor.getReputationData('example.com'); - expect(reputationData).toBeTruthy(); - - const summary = reputationMonitor.getReputationSummary(); - expect(summary.length).toBeGreaterThan(0); - - // Add and remove domains - reputationMonitor.addDomain('test.com'); - reputationMonitor.removeDomain('test.com'); - - // Test IPWarmupManager - ipWarmupManager.setActiveAllocationPolicy('balanced'); - - const bestIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - if (bestIP) { - ipWarmupManager.recordSend(bestIP); - const canSendMore = ipWarmupManager.canSendMoreToday(bestIP); - expect(typeof canSendMore).toEqual('boolean'); - } - - const stageCount = ipWarmupManager.getStageCount(); - expect(stageCount).toBeGreaterThan(0); -}); - -// Final clean-up test -tap.test('clean up after tests', async () => { - // No-op - just to make sure everything is cleaned up properly -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.dcrouter.email.ts b/test/test.dcrouter.email.ts deleted file mode 100644 index a2cd900..0000000 --- a/test/test.dcrouter.email.ts +++ /dev/null @@ -1,201 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as path from 'path'; -import * as fs from 'fs'; -import { - DcRouter, - type IDcRouterOptions, - type IEmailConfig, - type EmailProcessingMode, - type IDomainRule -} from '../ts/classes.dcrouter.ts'; - - -tap.test('DcRouter class - Custom email port configuration', async () => { - // Define custom port mapping - const customPortMapping = { - 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 - const emailConfig: IEmailConfig = { - ports: [25, 587, 465, 2525], // Added a non-standard port - hostname: 'mail.example.com', - maxMessageSize: 50 * 1024 * 1024, // 50MB - - defaultMode: 'forward' as EmailProcessingMode, - defaultServer: 'fallback-mail.example.com', - defaultPort: 25, - defaultTls: true, - - domainRules: [ - { - pattern: '*@example.com', - mode: 'forward' as EmailProcessingMode, - target: { - server: 'mail1.example.com', - port: 25, - useTls: true - } - }, - { - pattern: '*@example.org', - mode: 'mta' as EmailProcessingMode, - mtaOptions: { - domain: 'example.org', - allowLocalDelivery: true - } - } - ] - }; - - // Create custom email storage path - const customEmailsPath = path.join(process.cwd(), 'email'); - - // Ensure directory exists and is empty - if (fs.existsSync(customEmailsPath)) { - try { - fs.rmdirSync(customEmailsPath, { recursive: true }); - } catch (e) { - console.warn('Could not remove test directory:', e); - } - } - fs.mkdirSync(customEmailsPath, { recursive: true }); - - // Create DcRouter options with custom email port configuration - const options: IDcRouterOptions = { - emailConfig, - emailPortConfig: { - portMapping: customPortMapping, - portSettings: { - 2525: { - terminateTls: false, - routeName: 'custom-smtp-route' - } - }, - receivedEmailsPath: customEmailsPath - }, - 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); - expect(router.options.emailPortConfig.receivedEmailsPath).toEqual(customEmailsPath); - - // Test the generateEmailRoutes method - if (typeof router['generateEmailRoutes'] === 'function') { - const routes = router['generateEmailRoutes'](emailConfig); - - // Verify that all ports are configured - expect(routes.length).toBeGreaterThan(0); // At least some routes are configured - - // Check the custom port configuration - const customPortRoute = routes.find(r => { - 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.target.port).toEqual(12525); - - // Check standard port mappings - const smtpRoute = routes.find(r => { - const ports = r.match.ports; - return ports === 25 || (Array.isArray(ports) && (ports as number[]).includes(25)); - }); - expect(smtpRoute?.action.target.port).toEqual(11025); - - const submissionRoute = routes.find(r => { - const ports = r.match.ports; - return ports === 587 || (Array.isArray(ports) && (ports as number[]).includes(587)); - }); - expect(submissionRoute?.action.target.port).toEqual(11587); - } - - // Clean up - try { - fs.rmdirSync(customEmailsPath, { recursive: true }); - } catch (e) { - console.warn('Could not remove test directory in cleanup:', e); - } -}); - -tap.test('DcRouter class - Custom email storage path', async () => { - // Create custom email storage path - const customEmailsPath = path.join(process.cwd(), 'email'); - - // Ensure directory exists and is empty - if (fs.existsSync(customEmailsPath)) { - try { - fs.rmdirSync(customEmailsPath, { recursive: true }); - } catch (e) { - console.warn('Could not remove test directory:', e); - } - } - fs.mkdirSync(customEmailsPath, { recursive: true }); - - // Create a basic email configuration - const emailConfig: IEmailConfig = { - ports: [25], - hostname: 'mail.example.com', - defaultMode: 'mta' as EmailProcessingMode, - domainRules: [] - }; - - // Create DcRouter options with custom email storage path - const options: IDcRouterOptions = { - emailConfig, - emailPortConfig: { - receivedEmailsPath: customEmailsPath - }, - tls: { - contactEmail: 'test@example.com' - } - }; - - // Create DcRouter instance - const router = new DcRouter(options); - - // Start the router to initialize email services - await router.start(); - - // Verify that the custom email storage path was configured - expect(router.options.emailPortConfig?.receivedEmailsPath).toEqual(customEmailsPath); - - // Verify the directory exists - expect(fs.existsSync(customEmailsPath)).toEqual(true); - - // Verify unified email server was initialized - expect(router.unifiedEmailServer).toBeTruthy(); - - // Stop the router - await router.stop(); - - // Clean up - try { - fs.rmdirSync(customEmailsPath, { recursive: true }); - } catch (e) { - console.warn('Could not remove test directory in cleanup:', e); - } -}); - -// Final clean-up test -tap.test('clean up after tests', async () => { - // No-op - just to make sure everything is cleaned up properly -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -// Export a function to run all tests -export default tap.start(); \ No newline at end of file diff --git a/test/test.deliverability.ts b/test/test.deliverability.ts deleted file mode 100644 index fadb7fa..0000000 --- a/test/test.deliverability.ts +++ /dev/null @@ -1,55 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; - -// Import the components we want to test -import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.ts'; -import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.ts'; - -// Ensure test directories exist -paths.ensureDirectories(); - -// Test SenderReputationMonitor functionality -tap.test('SenderReputationMonitor should track sending events', async () => { - // Initialize monitor with test domain - const monitor = SenderReputationMonitor.getInstance({ - enabled: true, - domains: ['test-domain.com'] - }); - - // Record some events - monitor.recordSendEvent('test-domain.com', { type: 'sent', count: 100 }); - monitor.recordSendEvent('test-domain.com', { type: 'delivered', count: 95 }); - - // Get domain metrics - const metrics = monitor.getReputationData('test-domain.com'); - - // Verify metrics were recorded - if (metrics) { - expect(metrics.volume.sent).toEqual(100); - expect(metrics.volume.delivered).toEqual(95); - } -}); - -// Test IPWarmupManager functionality -tap.test('IPWarmupManager should handle IP allocation policies', async () => { - // Initialize warmup manager - const manager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2'], - targetDomains: ['test-domain.com'] - }); - - // Set allocation policy - manager.setActiveAllocationPolicy('balanced'); - - // Verify allocation methods work - const canSend = manager.canSendMoreToday('192.168.1.1'); - expect(typeof canSend).toEqual('boolean'); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.dns-manager-creation.ts b/test/test.dns-manager-creation.ts deleted file mode 100644 index f39bb07..0000000 --- a/test/test.dns-manager-creation.ts +++ /dev/null @@ -1,141 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { DnsManager } from '../ts/mail/routing/classes.dns.manager.ts'; -import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.ts'; -import { StorageManager } from '../ts/storage/classes.storagemanager.ts'; -import type { IEmailDomainConfig } from '../ts/mail/routing/interfaces.ts'; - -// Mock DcRouter with DNS server -class MockDcRouter { - public storageManager: StorageManager; - public dnsServer: any; - public options: any; - private dnsHandlers: Map = new Map(); - - constructor(testDir: string, dnsDomain?: string) { - this.storageManager = new StorageManager({ fsPath: testDir }); - this.options = { dnsDomain }; - - // Mock DNS server - this.dnsServer = { - registerHandler: (name: string, types: string[], handler: () => any) => { - const key = `${name}:${types.join(',')}`; - this.dnsHandlers.set(key, handler); - } - }; - } - - getDnsHandler(name: string, type: string): any { - const key = `${name}:${type}`; - return this.dnsHandlers.get(key); - } -} - -tap.test('DnsManager - Create Internal DNS Records', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-dns-manager-creation'); - const mockRouter = new MockDcRouter(testDir, 'ns.test.com') as any; - const dnsManager = new DnsManager(mockRouter); - - const domainConfigs: IEmailDomainConfig[] = [ - { - domain: 'test.example.com', - dnsMode: 'internal-dns', - dns: { - internal: { - mxPriority: 15, - ttl: 7200 - } - }, - dkim: { - selector: 'test2024', - keySize: 2048 - } - } - ]; - - // Create DNS records - await dnsManager.ensureDnsRecords(domainConfigs); - - // Verify MX record was registered - const mxHandler = mockRouter.getDnsHandler('test.example.com', 'MX'); - expect(mxHandler).toBeTruthy(); - const mxRecord = mxHandler(); - expect(mxRecord.type).toEqual('MX'); - expect(mxRecord.data.priority).toEqual(15); - expect(mxRecord.data.exchange).toEqual('test.example.com'); - expect(mxRecord.ttl).toEqual(7200); - - // Verify SPF record was registered - const txtHandler = mockRouter.getDnsHandler('test.example.com', 'TXT'); - expect(txtHandler).toBeTruthy(); - const spfRecord = txtHandler(); - expect(spfRecord.type).toEqual('TXT'); - expect(spfRecord.data).toEqual('v=spf1 a mx ~all'); - - // Verify DMARC record was registered - const dmarcHandler = mockRouter.getDnsHandler('_dmarc.test.example.com', 'TXT'); - expect(dmarcHandler).toBeTruthy(); - const dmarcRecord = dmarcHandler(); - expect(dmarcRecord.type).toEqual('TXT'); - expect(dmarcRecord.data).toContain('v=DMARC1'); - expect(dmarcRecord.data).toContain('p=none'); - - // Verify records were stored in StorageManager - const mxStored = await mockRouter.storageManager.getJSON('/email/dns/test.example.com/mx'); - expect(mxStored).toBeTruthy(); - expect(mxStored.priority).toEqual(15); - - const spfStored = await mockRouter.storageManager.getJSON('/email/dns/test.example.com/spf'); - expect(spfStored).toBeTruthy(); - expect(spfStored.data).toEqual('v=spf1 a mx ~all'); - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('DnsManager - Create DKIM Records', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-dns-manager-dkim'); - const keysDir = plugins.path.join(testDir, 'keys'); - await plugins.fs.promises.mkdir(keysDir, { recursive: true }); - - const mockRouter = new MockDcRouter(testDir, 'ns.test.com') as any; - const dnsManager = new DnsManager(mockRouter); - const dkimCreator = new DKIMCreator(keysDir, mockRouter.storageManager); - - const domainConfigs: IEmailDomainConfig[] = [ - { - domain: 'dkim.example.com', - dnsMode: 'internal-dns', - dkim: { - selector: 'mail2024', - keySize: 2048 - } - } - ]; - - // Generate DKIM keys first - await dkimCreator.handleDKIMKeysForDomain('dkim.example.com'); - - // Create DNS records including DKIM - await dnsManager.ensureDnsRecords(domainConfigs, dkimCreator); - - // Verify DKIM record was registered - const dkimHandler = mockRouter.getDnsHandler('mail2024._domainkey.dkim.example.com', 'TXT'); - expect(dkimHandler).toBeTruthy(); - const dkimRecord = dkimHandler(); - expect(dkimRecord.type).toEqual('TXT'); - expect(dkimRecord.data).toContain('v=DKIM1'); - expect(dkimRecord.data).toContain('k=rsa'); - expect(dkimRecord.data).toContain('p='); - - // Verify DKIM record was stored - const dkimStored = await mockRouter.storageManager.getJSON('/email/dns/dkim.example.com/dkim'); - expect(dkimStored).toBeTruthy(); - expect(dkimStored.name).toEqual('mail2024._domainkey.dkim.example.com'); - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.dns-mode-switching.ts b/test/test.dns-mode-switching.ts deleted file mode 100644 index 0f6908e..0000000 --- a/test/test.dns-mode-switching.ts +++ /dev/null @@ -1,257 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { StorageManager } from '../ts/storage/classes.storagemanager.ts'; -import { DnsManager } from '../ts/mail/routing/classes.dns.manager.ts'; -import { DomainRegistry } from '../ts/mail/routing/classes.domain.registry.ts'; -import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.ts'; -import type { IEmailDomainConfig } from '../ts/mail/routing/interfaces.ts'; - -// 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 - }; - } -} - -tap.test('DNS Mode Switching - Forward to Internal', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-dns-mode-switch-1'); - const keysDir = plugins.path.join(testDir, 'keys'); - await plugins.fs.promises.mkdir(keysDir, { recursive: true }); - - const mockRouter = new MockDcRouter(testDir, 'ns.test.com') as any; - const dkimCreator = new DKIMCreator(keysDir, mockRouter.storageManager); - - // Phase 1: Start with forward mode - let config: IEmailDomainConfig = { - domain: 'switchtest1.com', - dnsMode: 'forward', - dns: { - forward: { - skipDnsValidation: true - } - } - }; - - let registry = new DomainRegistry([config]); - let domainConfig = registry.getDomainConfig('switchtest1.com'); - - expect(domainConfig?.dnsMode).toEqual('forward'); - - // DKIM keys should still be generated for consistency - await dkimCreator.handleDKIMKeysForDomain('switchtest1.com'); - const keys = await dkimCreator.readDKIMKeys('switchtest1.com'); - expect(keys.privateKey).toBeTruthy(); - - // Phase 2: Switch to internal-dns mode - config = { - domain: 'switchtest1.com', - dnsMode: 'internal-dns', - dns: { - internal: { - mxPriority: 20, - ttl: 7200 - } - } - }; - - registry = new DomainRegistry([config]); - domainConfig = registry.getDomainConfig('switchtest1.com'); - - expect(domainConfig?.dnsMode).toEqual('internal-dns'); - expect(domainConfig?.dns?.internal?.mxPriority).toEqual(20); - - // DKIM keys should persist across mode switches - const keysAfterSwitch = await dkimCreator.readDKIMKeys('switchtest1.com'); - expect(keysAfterSwitch.privateKey).toEqual(keys.privateKey); - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('DNS Mode Switching - External to Forward', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-dns-mode-switch-2'); - const keysDir = plugins.path.join(testDir, 'keys'); - await plugins.fs.promises.mkdir(keysDir, { recursive: true }); - - const mockRouter = new MockDcRouter(testDir) as any; - const dkimCreator = new DKIMCreator(keysDir, mockRouter.storageManager); - - // Phase 1: Start with external-dns mode - let config: IEmailDomainConfig = { - domain: 'switchtest2.com', - dnsMode: 'external-dns', - dns: { - external: { - requiredRecords: ['MX', 'SPF', 'DKIM'] - } - }, - dkim: { - selector: 'custom2024', - keySize: 4096 - } - }; - - let registry = new DomainRegistry([config]); - let domainConfig = registry.getDomainConfig('switchtest2.com'); - - expect(domainConfig?.dnsMode).toEqual('external-dns'); - expect(domainConfig?.dkim?.selector).toEqual('custom2024'); - expect(domainConfig?.dkim?.keySize).toEqual(4096); - - // Generate DKIM keys (always uses default selector initially) - await dkimCreator.handleDKIMKeysForDomain('switchtest2.com'); - // For custom selector, we would need to implement key rotation - const dnsRecord = await dkimCreator.getDNSRecordForDomain('switchtest2.com'); - expect(dnsRecord.name).toContain('mta._domainkey'); - - // Phase 2: Switch to forward mode - config = { - domain: 'switchtest2.com', - dnsMode: 'forward', - dns: { - forward: { - targetDomain: 'mail.forward.com' - } - } - }; - - registry = new DomainRegistry([config]); - domainConfig = registry.getDomainConfig('switchtest2.com'); - - expect(domainConfig?.dnsMode).toEqual('forward'); - expect(domainConfig?.dns?.forward?.targetDomain).toEqual('mail.forward.com'); - - // DKIM configuration should revert to defaults - expect(domainConfig?.dkim?.selector).toEqual('default'); - expect(domainConfig?.dkim?.keySize).toEqual(2048); - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('DNS Mode Switching - Multiple Domains Different Modes', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-dns-mode-switch-3'); - const mockRouter = new MockDcRouter(testDir, 'ns.multi.com') as any; - - // Configure multiple domains with different modes - const domains: IEmailDomainConfig[] = [ - { - domain: 'forward.multi.com', - dnsMode: 'forward' - }, - { - domain: 'internal.multi.com', - dnsMode: 'internal-dns', - dns: { - internal: { - mxPriority: 5 - } - } - }, - { - domain: 'external.multi.com', - dnsMode: 'external-dns', - rateLimits: { - inbound: { - messagesPerMinute: 50 - } - } - } - ]; - - const registry = new DomainRegistry(domains); - - // Verify each domain has correct mode - expect(registry.getDomainConfig('forward.multi.com')?.dnsMode).toEqual('forward'); - expect(registry.getDomainConfig('internal.multi.com')?.dnsMode).toEqual('internal-dns'); - expect(registry.getDomainConfig('external.multi.com')?.dnsMode).toEqual('external-dns'); - - // Verify mode-specific configurations - expect(registry.getDomainConfig('internal.multi.com')?.dns?.internal?.mxPriority).toEqual(5); - expect(registry.getDomainConfig('external.multi.com')?.rateLimits?.inbound?.messagesPerMinute).toEqual(50); - - // Get domains by mode - const forwardDomains = registry.getDomainsByMode('forward'); - const internalDomains = registry.getDomainsByMode('internal-dns'); - const externalDomains = registry.getDomainsByMode('external-dns'); - - expect(forwardDomains.length).toEqual(1); - expect(forwardDomains[0].domain).toEqual('forward.multi.com'); - - expect(internalDomains.length).toEqual(1); - expect(internalDomains[0].domain).toEqual('internal.multi.com'); - - expect(externalDomains.length).toEqual(1); - expect(externalDomains[0].domain).toEqual('external.multi.com'); - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('DNS Mode Switching - Configuration Persistence', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-dns-mode-switch-4'); - const storage = new StorageManager({ fsPath: testDir }); - - // Save domain configuration - const config: IEmailDomainConfig = { - domain: 'persist.test.com', - dnsMode: 'internal-dns', - dns: { - internal: { - mxPriority: 15, - ttl: 1800 - } - }, - dkim: { - selector: 'persist2024', - rotateKeys: true, - rotationInterval: 30 - }, - rateLimits: { - outbound: { - messagesPerHour: 1000 - } - } - }; - - // Save to storage - await storage.setJSON('/email/domains/persist.test.com', config); - - // Simulate restart - load from storage - const loadedConfig = await storage.getJSON('/email/domains/persist.test.com'); - - expect(loadedConfig).toBeTruthy(); - expect(loadedConfig?.dnsMode).toEqual('internal-dns'); - expect(loadedConfig?.dns?.internal?.mxPriority).toEqual(15); - expect(loadedConfig?.dkim?.selector).toEqual('persist2024'); - expect(loadedConfig?.dkim?.rotateKeys).toEqual(true); - expect(loadedConfig?.rateLimits?.outbound?.messagesPerHour).toEqual(1000); - - // Update DNS mode - if (loadedConfig) { - loadedConfig.dnsMode = 'forward'; - loadedConfig.dns = { - forward: { - skipDnsValidation: false - } - }; - await storage.setJSON('/email/domains/persist.test.com', loadedConfig); - } - - // Load updated config - const updatedConfig = await storage.getJSON('/email/domains/persist.test.com'); - expect(updatedConfig?.dnsMode).toEqual('forward'); - expect(updatedConfig?.dns?.forward?.skipDnsValidation).toEqual(false); - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.dns-socket-handler.ts b/test/test.dns-socket-handler.ts deleted file mode 100644 index c277689..0000000 --- a/test/test.dns-socket-handler.ts +++ /dev/null @@ -1,169 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/classes.dcrouter.ts'; -import * as plugins from '../ts/plugins.ts'; - -let dcRouter: DcRouter; - -tap.test('should NOT instantiate DNS server when dnsDomain is not set', async () => { - dcRouter = new DcRouter({ - smartProxyConfig: { - routes: [] - } - }); - - await dcRouter.start(); - - // Check that DNS server is not created - expect((dcRouter as any).dnsServer).toBeUndefined(); - - await dcRouter.stop(); -}); - -tap.test('should instantiate DNS server when dnsDomain is set', async () => { - // Use a non-standard port to avoid conflicts - const testPort = 8443; - - dcRouter = new DcRouter({ - dnsDomain: 'dns.test.local', - smartProxyConfig: { - routes: [], - portMappings: { - 443: testPort // Map port 443 to test port - } - } as any - }); - - try { - await dcRouter.start(); - } catch (error) { - // If start fails due to port conflict, that's OK for this test - // We're mainly testing the route generation logic - } - - // Check that DNS server is created - expect((dcRouter as any).dnsServer).toBeDefined(); - - // Check routes were generated (even if SmartProxy failed to start) - const generatedRoutes = (dcRouter as any).generateDnsRoutes(); - expect(generatedRoutes.length).toEqual(2); // /dns-query and /resolve - - // Check that routes have socket-handler action - generatedRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('socket-handler'); - expect(route.action.socketHandler).toBeDefined(); - }); - - try { - await dcRouter.stop(); - } catch (error) { - // Ignore stop errors - } -}); - -tap.test('should create DNS routes with correct configuration', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.example.com', - smartProxyConfig: { - routes: [] - } - }); - - // Access the private method to generate routes - const dnsRoutes = (dcRouter as any).generateDnsRoutes(); - - expect(dnsRoutes.length).toEqual(2); - - // Check first route (dns-query) - const dnsQueryRoute = dnsRoutes.find((r: any) => r.name === 'dns-over-https-dns-query'); - expect(dnsQueryRoute).toBeDefined(); - expect(dnsQueryRoute.match.ports).toContain(443); - expect(dnsQueryRoute.match.domains).toContain('dns.example.com'); - expect(dnsQueryRoute.match.path).toEqual('/dns-query'); - - // Check second route (resolve) - const resolveRoute = dnsRoutes.find((r: any) => r.name === 'dns-over-https-resolve'); - expect(resolveRoute).toBeDefined(); - expect(resolveRoute.match.ports).toContain(443); - expect(resolveRoute.match.domains).toContain('dns.example.com'); - expect(resolveRoute.match.path).toEqual('/resolve'); -}); - -tap.test('DNS socket handler should handle sockets correctly', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.test.local', - smartProxyConfig: { - routes: [], - portMappings: { 443: 8444 } // Use different test port - } as any - }); - - try { - await dcRouter.start(); - } catch (error) { - // Ignore start errors for this test - } - - // Create a mock socket - const mockSocket = new plugins.net.Socket(); - let socketEnded = false; - let socketDestroyed = false; - - mockSocket.end = () => { - socketEnded = true; - }; - - mockSocket.destroy = () => { - socketDestroyed = true; - }; - - // Get the socket handler - const socketHandler = (dcRouter as any).createDnsSocketHandler(); - expect(socketHandler).toBeDefined(); - expect(typeof socketHandler).toEqual('function'); - - // Test with DNS server initialized - try { - await socketHandler(mockSocket); - } catch (error) { - // Expected - mock socket won't work properly - } - - // Socket should be handled by DNS server (even if it errors) - expect(socketHandler).toBeDefined(); - - try { - await dcRouter.stop(); - } catch (error) { - // Ignore stop errors - } -}); - -tap.test('DNS server should have manual HTTPS mode enabled', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.test.local' - }); - - // Don't actually start it to avoid port conflicts - // Instead, directly call the setup method - try { - await (dcRouter as any).setupDnsWithSocketHandler(); - } catch (error) { - // May fail but that's OK - } - - // Check that DNS server was created with correct options - const dnsServer = (dcRouter as any).dnsServer; - expect(dnsServer).toBeDefined(); - - // The important thing is that the DNS routes are created correctly - // and that the socket handler is set up - const socketHandler = (dcRouter as any).createDnsSocketHandler(); - expect(socketHandler).toBeDefined(); - expect(typeof socketHandler).toEqual('function'); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.dns-validation.ts b/test/test.dns-validation.ts deleted file mode 100644 index eee30c3..0000000 --- a/test/test.dns-validation.ts +++ /dev/null @@ -1,283 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { DnsManager } from '../ts/mail/routing/classes.dns.manager.ts'; -import { DomainRegistry } from '../ts/mail/routing/classes.domain.registry.ts'; -import { StorageManager } from '../ts/storage/classes.storagemanager.ts'; -import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.ts'; -import type { IEmailDomainConfig } from '../ts/mail/routing/interfaces.ts'; - -// 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 = new Map(); - private mockTxtRecords: Map = new Map(); - private mockMxRecords: Map = 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 { - return this.mockNsRecords.get(domain) || []; - } - - protected async resolveTxt(domain: string): Promise { - return this.mockTxtRecords.get(domain) || []; - } - - protected async resolveMx(domain: string): Promise { - 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(); \ No newline at end of file diff --git a/test/test.email-socket-handler.ts b/test/test.email-socket-handler.ts deleted file mode 100644 index 40fbf46..0000000 --- a/test/test.email-socket-handler.ts +++ /dev/null @@ -1,228 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/classes.dcrouter.ts'; -import * as plugins from '../ts/plugins.ts'; - -let dcRouter: DcRouter; - -tap.test('should use traditional port forwarding when useSocketHandler is false', async () => { - dcRouter = new DcRouter({ - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: false // Traditional mode - }, - smartProxyConfig: { - routes: [] - } - }); - - await dcRouter.start(); - - // Check that email server is created and listening on ports - const emailServer = (dcRouter as any).emailServer; - expect(emailServer).toBeDefined(); - - // Check SmartProxy routes are forward type - const smartProxy = (dcRouter as any).smartProxy; - const routes = smartProxy?.options?.routes || []; - const emailRoutes = routes.filter((route: any) => - route.name?.includes('-route') - ); - - emailRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('forward'); - expect(route.action.target).toBeDefined(); - expect(route.action.target.host).toEqual('localhost'); - }); - - await dcRouter.stop(); -}); - -tap.test('should use socket-handler mode when useSocketHandler is true', async () => { - dcRouter = new DcRouter({ - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: true // Socket-handler mode - }, - smartProxyConfig: { - routes: [] - } - }); - - await dcRouter.start(); - - // Check that email server is created - const emailServer = (dcRouter as any).emailServer; - expect(emailServer).toBeDefined(); - - // Check SmartProxy routes are socket-handler type - const smartProxy = (dcRouter as any).smartProxy; - const routes = smartProxy?.options?.routes || []; - const emailRoutes = routes.filter((route: any) => - route.name?.includes('-route') - ); - - emailRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('socket-handler'); - expect(route.action.socketHandler).toBeDefined(); - expect(typeof route.action.socketHandler).toEqual('function'); - }); - - await dcRouter.stop(); -}); - -tap.test('should generate correct email routes for each port', async () => { - const emailConfig = { - ports: [25, 587, 465], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: true - }; - - dcRouter = new DcRouter({ emailConfig }); - - // Access the private method to generate routes - const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig); - - expect(emailRoutes.length).toEqual(3); - - // Check SMTP route (port 25) - const smtpRoute = emailRoutes.find((r: any) => r.name === 'smtp-route'); - expect(smtpRoute).toBeDefined(); - expect(smtpRoute.match.ports).toContain(25); - expect(smtpRoute.action.type).toEqual('socket-handler'); - - // Check Submission route (port 587) - const submissionRoute = emailRoutes.find((r: any) => r.name === 'submission-route'); - expect(submissionRoute).toBeDefined(); - expect(submissionRoute.match.ports).toContain(587); - expect(submissionRoute.action.type).toEqual('socket-handler'); - - // Check SMTPS route (port 465) - const smtpsRoute = emailRoutes.find((r: any) => r.name === 'smtps-route'); - expect(smtpsRoute).toBeDefined(); - expect(smtpsRoute.match.ports).toContain(465); - expect(smtpsRoute.action.type).toEqual('socket-handler'); -}); - -tap.test('email socket handler should handle different ports correctly', async () => { - dcRouter = new DcRouter({ - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - // Test port 25 handler (plain SMTP) - const port25Handler = (dcRouter as any).createMailSocketHandler(25); - expect(port25Handler).toBeDefined(); - expect(typeof port25Handler).toEqual('function'); - - // Test port 465 handler (SMTPS - should wrap in TLS) - const port465Handler = (dcRouter as any).createMailSocketHandler(465); - expect(port465Handler).toBeDefined(); - expect(typeof port465Handler).toEqual('function'); - - await dcRouter.stop(); -}); - -tap.test('email server handleSocket method should work', async () => { - dcRouter = new DcRouter({ - emailConfig: { - ports: [25], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - const emailServer = (dcRouter as any).emailServer; - expect(emailServer).toBeDefined(); - expect(emailServer.handleSocket).toBeDefined(); - expect(typeof emailServer.handleSocket).toEqual('function'); - - // Create a mock socket - const mockSocket = new plugins.net.Socket(); - let socketDestroyed = false; - - mockSocket.destroy = () => { - socketDestroyed = true; - }; - - // Test handleSocket - try { - await emailServer.handleSocket(mockSocket, 25); - // It will fail because we don't have a real socket, but it should handle it gracefully - } catch (error) { - // Expected to error with mock socket - } - - await dcRouter.stop(); -}); - -tap.test('should not create SMTP servers when useSocketHandler is true', async () => { - dcRouter = new DcRouter({ - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - // The email server should not have any SMTP server instances - const emailServer = (dcRouter as any).emailServer; - expect(emailServer).toBeDefined(); - - // The servers array should be empty (no port binding) - expect(emailServer.servers).toBeDefined(); - expect(emailServer.servers.length).toEqual(0); - - await dcRouter.stop(); -}); - -tap.test('TLS handling should differ between ports', async () => { - const emailConfig = { - ports: [25, 465], - hostname: 'mail.test.local', - domains: ['test.local'], - routes: [], - useSocketHandler: false // Use traditional mode to check TLS config - }; - - dcRouter = new DcRouter({ emailConfig }); - - const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig); - - // Port 25 should use passthrough - const smtpRoute = emailRoutes.find((r: any) => r.match.ports[0] === 25); - expect(smtpRoute.action.tls.mode).toEqual('passthrough'); - - // Port 465 should use terminate - const smtpsRoute = emailRoutes.find((r: any) => r.match.ports[0] === 465); - expect(smtpsRoute.action.tls.mode).toEqual('terminate'); - expect(smtpsRoute.action.tls.certificate).toEqual('auto'); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.errors.ts b/test/test.errors.ts deleted file mode 100644 index 5f67276..0000000 --- a/test/test.errors.ts +++ /dev/null @@ -1,408 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as errors from '../ts/errors/index.ts'; -import { - PlatformError, - ValidationError, - NetworkError, - ResourceError, - OperationError -} from '../ts/errors/base.errors.ts'; -import { - ErrorSeverity, - ErrorCategory, - ErrorRecoverability -} from '../ts/errors/error.codes.ts'; -import { - EmailServiceError, - EmailTemplateError, - EmailValidationError, - EmailSendError, - EmailReceiveError -} from '../ts/errors/email.errors.ts'; -import { - MtaConnectionError, - MtaAuthenticationError, - MtaDeliveryError, - MtaConfigurationError -} from '../ts/errors/mta.errors.ts'; -import { - ErrorHandler -} from '../ts/errors/error-handler.ts'; - -// Test base error classes -tap.test('Base error classes should set properties correctly', async () => { - const message = 'Test error message'; - const code = 'TEST_ERROR_CODE'; - const context = { - component: 'TestComponent', - operation: 'testOperation', - data: { foo: 'bar' } - }; - - // Test PlatformError - const platformError = new PlatformError( - message, - code, - ErrorSeverity.MEDIUM, - ErrorCategory.OPERATION, - ErrorRecoverability.MAYBE_RECOVERABLE, - context - ); - - expect(platformError.message).toEqual(message); - expect(platformError.code).toEqual(code); - expect(platformError.severity).toEqual(ErrorSeverity.MEDIUM); - expect(platformError.category).toEqual(ErrorCategory.OPERATION); - expect(platformError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE); - expect(platformError.context?.component).toEqual(context.component); - expect(platformError.context?.operation).toEqual(context.operation); - expect(platformError.context?.data?.foo).toEqual('bar'); - expect(platformError.name).toEqual('PlatformError'); - - // Test ValidationError - const validationError = new ValidationError(message, code, context); - expect(validationError.category).toEqual(ErrorCategory.VALIDATION); - expect(validationError.severity).toEqual(ErrorSeverity.LOW); - - // Test NetworkError - const networkError = new NetworkError(message, code, context); - expect(networkError.category).toEqual(ErrorCategory.CONNECTIVITY); - expect(networkError.severity).toEqual(ErrorSeverity.MEDIUM); - expect(networkError.recoverability).toEqual(ErrorRecoverability.MAYBE_RECOVERABLE); - - // Test ResourceError - const resourceError = new ResourceError(message, code, context); - expect(resourceError.category).toEqual(ErrorCategory.RESOURCE); -}); - -// Test 7: Error withRetry() method -tap.test('PlatformError withRetry creates new instance with retry info', async () => { - const originalError = new EmailSendError('Send failed', { - data: { someData: true } - }); - - const retryError = originalError.withRetry(3, 1, 1000); - - // Verify it's a new instance - expect(retryError === originalError).toEqual(false); - expect(retryError).toBeInstanceOf(EmailSendError); - - // Verify original data is preserved - expect(retryError.context?.data?.someData).toEqual(true); - - // Verify retry info is added - expect(retryError.context?.retry?.maxRetries).toEqual(3); - expect(retryError.context?.retry?.currentRetry).toEqual(1); - expect(retryError.context?.retry?.retryDelay).toEqual(1000); - expect(retryError.context?.retry?.nextRetryAt).toBeTypeofNumber(); -}); - -// Test email error classes -tap.test('Email error classes should be properly constructed', async () => { - try { - // Test EmailServiceError - const emailServiceError = new EmailServiceError('Email service error', { - component: 'EmailService', - operation: 'sendEmail' - }); - expect(emailServiceError.code).toEqual('EMAIL_SERVICE_ERROR'); - expect(emailServiceError.name).toEqual('EmailServiceError'); - - // Test EmailTemplateError - const templateError = new EmailTemplateError('Template not found: welcome_email', { - data: { templateId: 'welcome_email' } - }); - expect(templateError.code).toEqual('EMAIL_TEMPLATE_ERROR'); - expect(templateError.context.data?.templateId).toEqual('welcome_email'); - - // Test EmailSendError with permanent flag - const permanentError = EmailSendError.permanent( - 'Invalid recipient: user@example.com', - { data: { details: 'DNS not found', recipient: 'user@example.com' } } - ); - expect(permanentError.code).toEqual('EMAIL_SEND_ERROR'); - expect(permanentError.isPermanent()).toEqual(true); - expect(permanentError.context.data?.permanent).toEqual(true); - - // Test EmailSendError with temporary flag and retry - const tempError = EmailSendError.temporary( - 'Server busy', - 3, - 0, - 1000, - { data: { server: 'smtp.example.com' } } - ); - expect(tempError.isPermanent()).toEqual(false); - expect(tempError.context.data?.permanent).toEqual(false); - expect(tempError.context.retry?.maxRetries).toEqual(3); - expect(tempError.shouldRetry()).toEqual(true); - } catch (error) { - console.error('Test failed with error:', error); - throw error; - } -}); - -// Test MTA error classes -tap.test('MTA error classes should be properly constructed', async () => { - try { - // Test MtaConnectionError - const dnsError = MtaConnectionError.dnsError('mail.example.com', new Error('DNS lookup failed')); - expect(dnsError.code).toEqual('MTA_CONNECTION_ERROR'); - expect(dnsError.category).toEqual(ErrorCategory.CONNECTIVITY); - expect(dnsError.context.data?.hostname).toEqual('mail.example.com'); - - // Test MtaTimeoutError via MtaConnectionError.timeout - const timeoutError = MtaConnectionError.timeout('mail.example.com', 25, 30000); - expect(timeoutError.code).toEqual('MTA_CONNECTION_ERROR'); - expect(timeoutError.context.data?.timeout).toEqual(30000); - - // Test MtaAuthenticationError - const authError = MtaAuthenticationError.invalidCredentials('mail.example.com', 'user@example.com'); - expect(authError.code).toEqual('MTA_AUTHENTICATION_ERROR'); - expect(authError.category).toEqual(ErrorCategory.AUTHENTICATION); - expect(authError.context.data?.username).toEqual('user@example.com'); - - // Test MtaDeliveryError - const permDeliveryError = MtaDeliveryError.permanent( - 'User unknown', - 'nonexistent@example.com', - '550', - '550 5.1.1 User unknown', - {} - ); - expect(permDeliveryError.code).toEqual('MTA_DELIVERY_ERROR'); - expect(permDeliveryError.isPermanent()).toEqual(true); - expect(permDeliveryError.getRecipientAddress()).toEqual('nonexistent@example.com'); - expect(permDeliveryError.getStatusCode()).toEqual('550'); - - // Test temporary delivery error with retry - const tempDeliveryError = MtaDeliveryError.temporary( - 'Mailbox temporarily unavailable', - 'user@example.com', - '450', - '450 4.2.1 Mailbox temporarily unavailable', - 3, - 1, - 5000 - ); - expect(tempDeliveryError.isPermanent()).toEqual(false); - expect(tempDeliveryError.shouldRetry()).toEqual(true); - expect(tempDeliveryError.context.retry?.currentRetry).toEqual(1); - expect(tempDeliveryError.context.retry?.maxRetries).toEqual(3); - } catch (error) { - console.error('MTA test failed with error:', error); - throw error; - } -}); - -// Test error handler utility -tap.test('ErrorHandler should properly handle and format errors', async () => { - // Configure error handler - ErrorHandler.configure({ - logErrors: false, // Disable for testing - includeStacksInProd: false, - retry: { - maxAttempts: 5, - baseDelay: 100, - maxDelay: 1000, - backoffFactor: 2 - } - }); - - // Test converting regular Error to PlatformError - const regularError = new Error('Something went wrong'); - const platformError = ErrorHandler.toPlatformError( - regularError, - 'PLATFORM_OPERATION_ERROR', - { component: 'TestHandler' } - ); - - expect(platformError).toBeInstanceOf(PlatformError); - expect(platformError.code).toEqual('PLATFORM_OPERATION_ERROR'); - expect(platformError.context?.component).toEqual('TestHandler'); - - // Test formatting error for API response - const formattedError = ErrorHandler.formatErrorForResponse(platformError, true); - expect(formattedError.code).toEqual('PLATFORM_OPERATION_ERROR'); - expect(formattedError.message).toEqual('An unexpected error occurred.'); - expect(formattedError.details?.rawMessage).toEqual('Something went wrong'); - - // Test executing a function with error handling - let executed = false; - try { - await ErrorHandler.execute(async () => { - executed = true; - throw new Error('Execution failed'); - }, 'TEST_EXECUTION_ERROR', { operation: 'testExecution' }); - } catch (error) { - expect(error).toBeInstanceOf(PlatformError); - expect(error.code).toEqual('TEST_EXECUTION_ERROR'); - expect(error.context.operation).toEqual('testExecution'); - } - expect(executed).toEqual(true); - - // Test executeWithRetry successful after retries - let attempts = 0; - const result = await ErrorHandler.executeWithRetry( - async () => { - attempts++; - if (attempts < 3) { - throw new Error('Temporary failure'); - } - return 'success'; - }, - 'TEST_RETRY_ERROR', - { - maxAttempts: 5, - baseDelay: 10, // Use small delay for tests - retryableErrorPatterns: [/Temporary failure/], // Add pattern to make error retryable - onRetry: (error, attempt, delay) => { - expect(error).toBeInstanceOf(PlatformError); - expect(attempt).toBeGreaterThan(0); - expect(delay).toBeGreaterThan(0); - } - } - ); - - expect(result).toEqual('success'); - expect(attempts).toEqual(3); - - // Test executeWithRetry that fails after max attempts - attempts = 0; - try { - await ErrorHandler.executeWithRetry( - async () => { - attempts++; - throw new Error('Persistent failure'); - }, - 'TEST_RETRY_ERROR', - { - maxAttempts: 3, - baseDelay: 10, - retryableErrorPatterns: [/Persistent failure/] // Make error retryable so it tries all attempts - } - ); - } catch (error) { - expect(error).toBeInstanceOf(PlatformError); - expect(attempts).toEqual(3); - } -}); - -// Test retry utilities -tap.test('Error retry utilities should work correctly', async () => { - let attempts = 0; - const start = Date.now(); - - try { - await errors.retry( - async () => { - attempts++; - if (attempts < 3) { - throw new Error('Temporary error'); - } - return 'success'; - }, - { - maxRetries: 5, - initialDelay: 20, - backoffFactor: 1.5, - retryableErrors: [/Temporary/] - } - ); - } catch (e) { - // Should not reach here - expect(false).toEqual(true); - } - - expect(attempts).toEqual(3); - - // Test retry with non-retryable error - attempts = 0; - try { - await errors.retry( - async () => { - attempts++; - throw new Error('Critical error'); - }, - { - maxRetries: 3, - initialDelay: 10, - retryableErrors: [/Temporary/] // Won't match "Critical" - } - ); - } catch (error) { - expect(error.message).toEqual('Critical error'); - expect(attempts).toEqual(1); // Should only attempt once - } -}); - -// Helper function that will reject first n times, then resolve -interface FlakyFunction { - (failTimes: number, result?: any): Promise; - counter: number; - reset: () => void; -} - -const flaky: FlakyFunction = Object.assign( - async function (failTimes: number, result: any = 'success'): Promise { - if (flaky.counter < failTimes) { - flaky.counter++; - throw new Error(`Flaky failure ${flaky.counter}`); - } - return result; - }, - { - counter: 0, - reset: () => { flaky.counter = 0; } - } -); - -// Test error wrapping and retry combination -tap.test('Error handling can be combined with retry for robust operations', async () => { - // Reset counter for the test - flaky.reset(); - - // Create a wrapped version of the flaky function - const wrapped = errors.withErrorHandling( - () => flaky(2, 'wrapped success'), - 'TEST_WRAPPED_ERROR', - { component: 'TestComponent' } - ); - - // Execute with retry - const result = await errors.retry( - wrapped, - { - maxRetries: 3, - initialDelay: 10, - retryableErrors: [/Flaky failure/] - } - ); - expect(result).toEqual('wrapped success'); - expect(flaky.counter).toEqual(2); - - // Reset and test failure case - flaky.reset(); - - try { - await errors.retry( - () => flaky(5, 'never reached'), - { - maxRetries: 2, // Only retry twice, but we need 5 attempts to succeed - initialDelay: 10, - retryableErrors: [/Flaky failure/] // Add pattern to make it retry - } - ); - // Should not reach here - expect(false).toEqual(true); - } catch (error) { - expect(error.message).toContain('Flaky failure'); - expect(flaky.counter).toEqual(3); // Initial + 2 retries = 3 attempts - } -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.integration.storage.ts b/test/test.integration.storage.ts deleted file mode 100644 index 83d5c07..0000000 --- a/test/test.integration.storage.ts +++ /dev/null @@ -1,313 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { StorageManager } from '../ts/storage/classes.storagemanager.ts'; -import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.ts'; -import { BounceManager } from '../ts/mail/core/classes.bouncemanager.ts'; -import { EmailRouter } from '../ts/mail/routing/classes.email.router.ts'; -import type { IEmailRoute } from '../ts/mail/routing/interfaces.ts'; - -tap.test('Storage Persistence Across Restarts', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-integration-persistence'); - - // Phase 1: Create storage and write data - { - const storage = new StorageManager({ fsPath: testDir }); - - // Write some test data - await storage.set('/test/key1', 'value1'); - await storage.setJSON('/test/json', { data: 'test', count: 42 }); - await storage.set('/other/key2', 'value2'); - } - - // Phase 2: Create new instance and verify data persists - { - const storage = new StorageManager({ fsPath: testDir }); - - // Verify data persists - const value1 = await storage.get('/test/key1'); - expect(value1).toEqual('value1'); - - const jsonData = await storage.getJSON('/test/json'); - expect(jsonData).toEqual({ data: 'test', count: 42 }); - - const value2 = await storage.get('/other/key2'); - expect(value2).toEqual('value2'); - - // Test list operation - const testKeys = await storage.list('/test'); - expect(testKeys.length).toEqual(2); - expect(testKeys).toContain('/test/key1'); - expect(testKeys).toContain('/test/json'); - } - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('DKIM Storage Integration', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-integration-dkim'); - const keysDir = plugins.path.join(testDir, 'keys'); - - // Phase 1: Generate DKIM keys with storage - { - const storage = new StorageManager({ fsPath: testDir }); - const dkimCreator = new DKIMCreator(keysDir, storage); - - await dkimCreator.handleDKIMKeysForDomain('storage.example.com'); - - // Verify keys exist - const keys = await dkimCreator.readDKIMKeys('storage.example.com'); - expect(keys.privateKey).toBeTruthy(); - expect(keys.publicKey).toBeTruthy(); - } - - // Phase 2: New instance should find keys in storage - { - const storage = new StorageManager({ fsPath: testDir }); - const dkimCreator = new DKIMCreator(keysDir, storage); - - // Keys should be loaded from storage - const keys = await dkimCreator.readDKIMKeys('storage.example.com'); - expect(keys.privateKey).toBeTruthy(); - expect(keys.publicKey).toBeTruthy(); - expect(keys.privateKey).toContain('BEGIN RSA PRIVATE KEY'); - } - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('Bounce Manager Storage Integration', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-integration-bounce'); - - // Phase 1: Add to suppression list with storage - { - const storage = new StorageManager({ fsPath: testDir }); - const bounceManager = new BounceManager({ - storageManager: storage - }); - - // Add emails to suppression list - bounceManager.addToSuppressionList('bounce1@example.com', 'Hard bounce: invalid_recipient'); - bounceManager.addToSuppressionList('bounce2@example.com', 'Soft bounce: temporary', Date.now() + 3600000); - - // Verify suppression - expect(bounceManager.isEmailSuppressed('bounce1@example.com')).toEqual(true); - expect(bounceManager.isEmailSuppressed('bounce2@example.com')).toEqual(true); - } - - // Wait a moment to ensure async save completes - await new Promise(resolve => setTimeout(resolve, 100)); - - // Phase 2: New instance should load suppression list from storage - { - const storage = new StorageManager({ fsPath: testDir }); - const bounceManager = new BounceManager({ - storageManager: storage - }); - - // Wait for async load - await new Promise(resolve => setTimeout(resolve, 100)); - - // Verify persistence - expect(bounceManager.isEmailSuppressed('bounce1@example.com')).toEqual(true); - expect(bounceManager.isEmailSuppressed('bounce2@example.com')).toEqual(true); - expect(bounceManager.isEmailSuppressed('notbounced@example.com')).toEqual(false); - - // Check suppression info - const info1 = bounceManager.getSuppressionInfo('bounce1@example.com'); - expect(info1).toBeTruthy(); - expect(info1?.reason).toContain('Hard bounce'); - expect(info1?.expiresAt).toBeUndefined(); // Permanent - - const info2 = bounceManager.getSuppressionInfo('bounce2@example.com'); - expect(info2).toBeTruthy(); - expect(info2?.reason).toContain('Soft bounce'); - expect(info2?.expiresAt).toBeGreaterThan(Date.now()); - } - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('Email Router Storage Integration', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-integration-router'); - - const testRoutes: IEmailRoute[] = [ - { - name: 'test-route-1', - match: { recipients: '*@test.com' }, - action: { type: 'forward', forward: { host: 'test.server.com', port: 25 } }, - priority: 100 - }, - { - name: 'test-route-2', - match: { senders: '*@internal.com' }, - action: { type: 'process', process: { scan: true, dkim: true } }, - priority: 50 - } - ]; - - // Phase 1: Save routes with storage - { - const storage = new StorageManager({ fsPath: testDir }); - const router = new EmailRouter([], { - storageManager: storage, - persistChanges: true - }); - - // Add routes - await router.addRoute(testRoutes[0]); - await router.addRoute(testRoutes[1]); - - // Verify routes - const routes = router.getRoutes(); - expect(routes.length).toEqual(2); - expect(routes[0].name).toEqual('test-route-1'); // Higher priority first - expect(routes[1].name).toEqual('test-route-2'); - } - - // Phase 2: New instance should load routes from storage - { - const storage = new StorageManager({ fsPath: testDir }); - const router = new EmailRouter([], { - storageManager: storage, - persistChanges: true - }); - - // Wait for async load - await new Promise(resolve => setTimeout(resolve, 100)); - - // Manually load routes (since constructor load is fire-and-forget) - await router.loadRoutes({ replace: true }); - - // Verify persistence - const routes = router.getRoutes(); - expect(routes.length).toEqual(2); - expect(routes[0].name).toEqual('test-route-1'); - expect(routes[0].priority).toEqual(100); - expect(routes[1].name).toEqual('test-route-2'); - expect(routes[1].priority).toEqual(50); - - // Test route retrieval - const route1 = router.getRoute('test-route-1'); - expect(route1).toBeTruthy(); - expect(route1?.match.recipients).toEqual('*@test.com'); - } - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('Storage Backend Switching', async () => { - const testDir = plugins.path.join(paths.dataDir, '.test-integration-switching'); - const testData = { key: 'value', nested: { data: true } }; - - // Phase 1: Start with memory storage - const memoryStore = new Map(); - { - const storage = new StorageManager(); // Memory backend - await storage.setJSON('/switch/test', testData); - - // Verify it's in memory - expect(storage.getBackend()).toEqual('memory'); - } - - // Phase 2: Switch to custom backend - { - const storage = new StorageManager({ - readFunction: async (key) => memoryStore.get(key) || null, - writeFunction: async (key, value) => { memoryStore.set(key, value); } - }); - - // Write data - await storage.setJSON('/switch/test', testData); - - // Verify backend - expect(storage.getBackend()).toEqual('custom'); - expect(memoryStore.has('/switch/test')).toEqual(true); - } - - // Phase 3: Switch to filesystem - { - const storage = new StorageManager({ fsPath: testDir }); - - // Migrate data from custom backend - const dataStr = memoryStore.get('/switch/test'); - if (dataStr) { - await storage.set('/switch/test', dataStr); - } - - // Verify data migrated - const data = await storage.getJSON('/switch/test'); - expect(data).toEqual(testData); - expect(storage.getBackend()).toEqual('filesystem'); // fsPath is now properly reported as filesystem - } - - // Clean up - await plugins.fs.promises.rm(testDir, { recursive: true, force: true }).catch(() => {}); -}); - -tap.test('Data Migration Between Backends', async () => { - const testDir1 = plugins.path.join(paths.dataDir, '.test-migration-source'); - const testDir2 = plugins.path.join(paths.dataDir, '.test-migration-dest'); - - // Create test data structure - const testData = { - '/config/app': JSON.stringify({ name: 'test-app', version: '1.0.0' }), - '/config/database': JSON.stringify({ host: 'localhost', port: 5432 }), - '/data/users/1': JSON.stringify({ id: 1, name: 'User One' }), - '/data/users/2': JSON.stringify({ id: 2, name: 'User Two' }), - '/logs/app.log': 'Log entry 1\nLog entry 2\nLog entry 3' - }; - - // Phase 1: Populate source storage - { - const source = new StorageManager({ fsPath: testDir1 }); - - for (const [key, value] of Object.entries(testData)) { - await source.set(key, value); - } - - // Verify data written - const keys = await source.list('/'); - expect(keys.length).toBeGreaterThanOrEqual(5); - } - - // Phase 2: Migrate to destination - { - const source = new StorageManager({ fsPath: testDir1 }); - const dest = new StorageManager({ fsPath: testDir2 }); - - // List all keys from source - const allKeys = await source.list('/'); - - // Migrate each key - for (const key of allKeys) { - const value = await source.get(key); - if (value !== null) { - await dest.set(key, value); - } - } - - // Verify migration - for (const [key, expectedValue] of Object.entries(testData)) { - const value = await dest.get(key); - expect(value).toEqual(expectedValue); - } - - // Verify structure preserved - const configKeys = await dest.list('/config'); - expect(configKeys.length).toEqual(2); - - const userKeys = await dest.list('/data/users'); - expect(userKeys.length).toEqual(2); - } - - // Clean up - await plugins.fs.promises.rm(testDir1, { recursive: true, force: true }).catch(() => {}); - await plugins.fs.promises.rm(testDir2, { recursive: true, force: true }).catch(() => {}); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.integration.ts b/test/test.integration.ts deleted file mode 100644 index 0dbf250..0000000 --- a/test/test.integration.ts +++ /dev/null @@ -1,75 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -// SzPlatformService doesn't exist in codebase - using DcRouter instead for integration tests -import DcRouter from '../ts/classes.dcrouter.ts'; -import { BounceManager } from '../ts/mail/core/classes.bouncemanager.ts'; -import { smtpClientMod } from '../ts/mail/delivery/index.ts'; -import { SmtpServer } from '../ts/mail/delivery/smtpserver/smtp-server.ts'; - -// Test the new integration architecture -tap.test('should be able to create an SMTP server', async (tools) => { - // Create an SMTP server - const smtpServer = new SmtpServer({ - options: { - port: 10025, - hostname: 'test.example.com', - key: '', - cert: '' - } - }); - - // Verify it was created properly - expect(smtpServer).toBeTruthy(); - expect(smtpServer.options.port).toEqual(10025); - expect(smtpServer.options.hostname).toEqual('test.example.com'); -}); - - -tap.test('DcRouter should support email configuration', async (tools) => { - // Create a DcRouter with email config - const dcRouter = new DcRouter({ - emailConfig: { - useEmail: true, - domainRules: [{ - // name: 'test-rule', // not part of IDomainRule - match: { - senderPattern: '.*@test.com', - }, - actions: [] - }] - } - }); - - // Verify it was created properly - expect(dcRouter).toBeTruthy(); -}); - -tap.test('SMTP client should be able to connect to SMTP server', async (tools) => { - // Create an SMTP client - const options = { - host: 'smtp.test.com', - port: 587, - secure: false, - auth: { - user: 'test@example.com', - pass: 'testpass' - }, - connectionTimeout: 5000, - socketTimeout: 5000 - }; - - const smtpClient = smtpClientMod.createSmtpClient(options); - - // Verify it was created properly - expect(smtpClient).toBeTruthy(); - // Since options are not exposed, just verify the client was created - expect(typeof smtpClient.sendMail).toEqual('function'); - expect(typeof smtpClient.getPoolStatus).toEqual('function'); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -// Export for tapbundle execution -export default tap.start(); \ No newline at end of file diff --git a/test/test.ipwarmupmanager.ts b/test/test.ipwarmupmanager.ts deleted file mode 100644 index c6428f6..0000000 --- a/test/test.ipwarmupmanager.ts +++ /dev/null @@ -1,323 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.ts'; - -// Cleanup any temporary test data -const cleanupTestData = () => { - const warmupDataPath = plugins.path.join(paths.dataDir, 'warmup'); - if (plugins.fs.existsSync(warmupDataPath)) { - // Remove the directory recursively using fs instead of smartfile - plugins.fs.rmSync(warmupDataPath, { recursive: true, force: true }); - } -}; - -// Helper to reset the singleton instance between tests -const resetSingleton = () => { - // @ts-ignore - accessing private static field for testing - IPWarmupManager.instance = null; -}; - -// Before running any tests -tap.test('setup', async () => { - cleanupTestData(); -}); - -// Test initialization of IPWarmupManager -tap.test('should initialize IPWarmupManager with default settings', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance(); - - expect(ipWarmupManager).toBeTruthy(); - expect(typeof ipWarmupManager.getBestIPForSending).toEqual('function'); - expect(typeof ipWarmupManager.canSendMoreToday).toEqual('function'); - expect(typeof ipWarmupManager.getStageCount).toEqual('function'); - expect(typeof ipWarmupManager.setActiveAllocationPolicy).toEqual('function'); -}); - -// Test initialization with custom settings -tap.test('should initialize IPWarmupManager with custom settings', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2'], - targetDomains: ['example.com', 'test.com'], - fallbackPercentage: 75 - }); - - // Test setting allocation policy - ipWarmupManager.setActiveAllocationPolicy('roundRobin'); - - // Get best IP for sending - const bestIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - // Check if we can send more today - const canSendMore = ipWarmupManager.canSendMoreToday('192.168.1.1'); - - // Check stage count - const stageCount = ipWarmupManager.getStageCount(); - expect(typeof stageCount).toEqual('number'); -}); - -// Test IP allocation policies -tap.test('should allocate IPs using balanced policy', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'], - targetDomains: ['example.com', 'test.com'] - // Remove allocationPolicy which is not in the interface - }); - - ipWarmupManager.setActiveAllocationPolicy('balanced'); - - // Use getBestIPForSending multiple times and check if all IPs are used - const usedIPs = new Set(); - for (let i = 0; i < 30; i++) { - const ip = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - if (ip) usedIPs.add(ip); - } - - // We should use at least 2 different IPs with balanced policy - expect(usedIPs.size >= 2).toEqual(true); -}); - -// Test round robin allocation policy -tap.test('should allocate IPs using round robin policy', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'], - targetDomains: ['example.com', 'test.com'] - // Remove allocationPolicy which is not in the interface - }); - - ipWarmupManager.setActiveAllocationPolicy('roundRobin'); - - // First few IPs should rotate through the available IPs - const firstIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - const secondIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - const thirdIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - // Round robin should give us different IPs for consecutive calls - expect(firstIP !== secondIP).toEqual(true); - - // With 3 IPs, the fourth call should cycle back to one of the IPs - const fourthIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - // Check that the fourth IP is one of the 3 valid IPs - expect(['192.168.1.1', '192.168.1.2', '192.168.1.3'].includes(fourthIP)).toEqual(true); -}); - -// Test dedicated domain allocation policy -tap.test('should allocate IPs using dedicated domain policy', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'], - targetDomains: ['example.com', 'test.com', 'other.com'] - // Remove allocationPolicy which is not in the interface - }); - - ipWarmupManager.setActiveAllocationPolicy('dedicated'); - - // Instead of mapDomainToIP which doesn't exist, we'll simulate domain mapping - // by making dedicated calls per domain - we can't call the internal method directly - - // Each domain should get its dedicated IP - const exampleIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@gmail.com'], - domain: 'example.com' - }); - - const testIP = ipWarmupManager.getBestIPForSending({ - from: 'test@test.com', - to: ['recipient@gmail.com'], - domain: 'test.com' - }); - - const otherIP = ipWarmupManager.getBestIPForSending({ - from: 'test@other.com', - to: ['recipient@gmail.com'], - domain: 'other.com' - }); - - // Since we're not actually mapping domains to IPs, we can only test if they return valid IPs - // The original assertions have been modified since we can't guarantee which IP will be returned - expect(exampleIP).toBeTruthy(); - expect(testIP).toBeTruthy(); - expect(otherIP).toBeTruthy(); -}); - -// Test daily sending limits -tap.test('should enforce daily sending limits', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1'], - targetDomains: ['example.com'] - // Remove allocationPolicy which is not in the interface - }); - - // Override the warmup stage for testing - // @ts-ignore - accessing private method for testing - ipWarmupManager.warmupStatuses.set('192.168.1.1', { - ipAddress: '192.168.1.1', - isActive: true, - currentStage: 1, - startDate: new Date(), - currentStageStartDate: new Date(), - targetCompletionDate: new Date(), - currentDailyAllocation: 5, - sentInCurrentStage: 0, - totalSent: 0, - dailyStats: [], - metrics: { - openRate: 0, - bounceRate: 0, - complaintRate: 0 - } - }); - - // Set a very low daily limit for testing - // @ts-ignore - accessing private method for testing - ipWarmupManager.config.stages = [ - { stage: 1, maxDailyVolume: 5, durationDays: 5, targetMetrics: { maxBounceRate: 8, minOpenRate: 15 } } - ]; - - // First pass: should be able to get an IP - const ip = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - expect(ip === '192.168.1.1').toEqual(true); - - // Record 5 sends to reach the daily limit - for (let i = 0; i < 5; i++) { - ipWarmupManager.recordSend('192.168.1.1'); - } - - // Check if we can send more today - const canSendMore = ipWarmupManager.canSendMoreToday('192.168.1.1'); - expect(canSendMore).toEqual(false); - - // After reaching limit, getBestIPForSending should return null - // since there are no available IPs - const sixthIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - expect(sixthIP === null).toEqual(true); -}); - -// Test recording sends -tap.test('should record send events correctly', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2'], - targetDomains: ['example.com'], - }); - - // Set allocation policy - ipWarmupManager.setActiveAllocationPolicy('balanced'); - - // Get an IP for sending - const ip = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - // If we got an IP, record some sends - if (ip) { - // Record a few sends - for (let i = 0; i < 5; i++) { - ipWarmupManager.recordSend(ip); - } - - // Check if we can still send more - const canSendMore = ipWarmupManager.canSendMoreToday(ip); - expect(typeof canSendMore).toEqual('boolean'); - } -}); - -// Test that DedicatedDomainPolicy assigns IPs correctly -tap.test('should assign IPs using dedicated domain policy', async () => { - resetSingleton(); - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2', '192.168.1.3'], - targetDomains: ['example.com', 'test.com', 'other.com'] - }); - - // Set allocation policy to dedicated domains - ipWarmupManager.setActiveAllocationPolicy('dedicated'); - - // Check allocation by querying for different domains - const ip1 = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - const ip2 = ipWarmupManager.getBestIPForSending({ - from: 'test@test.com', - to: ['recipient@test.com'], - domain: 'test.com' - }); - - // If we got IPs, they should be consistently assigned - if (ip1 && ip2) { - // Requesting the same domain again should return the same IP - const ip1again = ipWarmupManager.getBestIPForSending({ - from: 'another@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - expect(ip1again === ip1).toEqual(true); - } -}); - -// After all tests, clean up -tap.test('cleanup', async () => { - cleanupTestData(); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.jwt-auth.ts b/test/test.jwt-auth.ts deleted file mode 100644 index d58b6b0..0000000 --- a/test/test.jwt-auth.ts +++ /dev/null @@ -1,130 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/index.ts'; -import { TypedRequest } from '@api.global/typedrequest'; -import * as interfaces from '../ts_interfaces/index.ts'; - -let testDcRouter: DcRouter; -let identity: interfaces.data.IIdentity; - -tap.test('should start DCRouter with OpsServer', async () => { - testDcRouter = new DcRouter({ - // Minimal config for testing - }); - - await testDcRouter.start(); - expect(testDcRouter.opsServer).toBeInstanceOf(Object); -}); - -tap.test('should login with admin credentials and receive JWT', async () => { - const loginRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'adminLoginWithUsernameAndPassword' - ); - - const response = await loginRequest.fire({ - username: 'admin', - password: 'admin' - }); - - expect(response).toHaveProperty('identity'); - expect(response.identity).toHaveProperty('jwt'); - expect(response.identity).toHaveProperty('userId'); - expect(response.identity).toHaveProperty('name'); - expect(response.identity).toHaveProperty('expiresAt'); - expect(response.identity).toHaveProperty('role'); - expect(response.identity.role).toEqual('admin'); - - identity = response.identity; - console.log('JWT:', identity.jwt); -}); - -tap.test('should verify valid JWT identity', async () => { - const verifyRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'verifyIdentity' - ); - - const response = await verifyRequest.fire({ - identity - }); - - expect(response).toHaveProperty('valid'); - expect(response.valid).toBeTrue(); - expect(response).toHaveProperty('identity'); - expect(response.identity.userId).toEqual(identity.userId); -}); - -tap.test('should reject invalid JWT', async () => { - const verifyRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'verifyIdentity' - ); - - const response = await verifyRequest.fire({ - identity: { - ...identity, - jwt: 'invalid.jwt.token' - } - }); - - expect(response).toHaveProperty('valid'); - expect(response.valid).toBeFalse(); -}); - -tap.test('should verify JWT matches identity data', async () => { - const verifyRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'verifyIdentity' - ); - - // The response should contain the same identity data as the JWT - const response = await verifyRequest.fire({ - identity - }); - - expect(response).toHaveProperty('valid'); - expect(response.valid).toBeTrue(); - expect(response.identity.expiresAt).toEqual(identity.expiresAt); - expect(response.identity.userId).toEqual(identity.userId); -}); - -tap.test('should handle logout', async () => { - const logoutRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'adminLogout' - ); - - const response = await logoutRequest.fire({ - identity - }); - - expect(response).toHaveProperty('success'); - expect(response.success).toBeTrue(); -}); - -tap.test('should reject wrong credentials', async () => { - const loginRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'adminLoginWithUsernameAndPassword' - ); - - let errorOccurred = false; - try { - await loginRequest.fire({ - username: 'admin', - password: 'wrongpassword' - }); - } catch (error) { - errorOccurred = true; - // TypedResponseError is thrown - expect(error).toBeTruthy(); - } - - expect(errorOccurred).toBeTrue(); -}); - -tap.test('should stop DCRouter', async () => { - await testDcRouter.stop(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.minimal.ts b/test/test.minimal.ts deleted file mode 100644 index 814c64c..0000000 --- a/test/test.minimal.ts +++ /dev/null @@ -1,66 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.ts'; -import { IPWarmupManager } from '../ts/deliverability/classes.ipwarmupmanager.ts'; - -/** - * Basic test to check if our integrated classes work correctly - */ -tap.test('verify that SenderReputationMonitor and IPWarmupManager are functioning', async (tools) => { - // Create instances of both classes - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: true, - domains: ['example.com'] - }); - - const ipWarmupManager = IPWarmupManager.getInstance({ - enabled: true, - ipAddresses: ['192.168.1.1', '192.168.1.2'], - targetDomains: ['example.com'] - }); - - // Test SenderReputationMonitor - reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 }); - reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 }); - - const reputationData = reputationMonitor.getReputationData('example.com'); - const summary = reputationMonitor.getReputationSummary(); - - // Basic checks - expect(reputationData).toBeTruthy(); - expect(summary.length).toBeGreaterThan(0); - - // Add and remove domains - reputationMonitor.addDomain('test.com'); - reputationMonitor.removeDomain('test.com'); - - // Test IPWarmupManager - ipWarmupManager.setActiveAllocationPolicy('balanced'); - - const bestIP = ipWarmupManager.getBestIPForSending({ - from: 'test@example.com', - to: ['recipient@test.com'], - domain: 'example.com' - }); - - if (bestIP) { - ipWarmupManager.recordSend(bestIP); - const canSendMore = ipWarmupManager.canSendMoreToday(bestIP); - expect(canSendMore !== undefined).toEqual(true); - } - - const stageCount = ipWarmupManager.getStageCount(); - expect(stageCount).toBeGreaterThan(0); -}); - -// Final clean-up test -tap.test('clean up after tests', async () => { - // No-op - just to make sure everything is cleaned up properly -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.opsserver-api.ts b/test/test.opsserver-api.ts deleted file mode 100644 index 0d2b472..0000000 --- a/test/test.opsserver-api.ts +++ /dev/null @@ -1,83 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/index.ts'; -import { TypedRequest } from '@api.global/typedrequest'; -import * as interfaces from '../ts_interfaces/index.ts'; - -let testDcRouter: DcRouter; - -tap.test('should start DCRouter with OpsServer', async () => { - testDcRouter = new DcRouter({ - // Minimal config for testing - }); - - await testDcRouter.start(); - expect(testDcRouter.opsServer).toBeInstanceOf(Object); -}); - -tap.test('should respond to health status request', async () => { - const healthRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'getHealthStatus' - ); - - const response = await healthRequest.fire({ - detailed: false - }); - - expect(response).toHaveProperty('health'); - expect(response.health.healthy).toBeTrue(); - expect(response.health.services).toHaveProperty('OpsServer'); -}); - -tap.test('should respond to server statistics request', async () => { - const statsRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'getServerStatistics' - ); - - const response = await statsRequest.fire({ - includeHistory: false - }); - - expect(response).toHaveProperty('stats'); - expect(response.stats).toHaveProperty('uptime'); - expect(response.stats).toHaveProperty('cpuUsage'); - expect(response.stats).toHaveProperty('memoryUsage'); -}); - -tap.test('should respond to configuration request', async () => { - const configRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'getConfiguration' - ); - - const response = await configRequest.fire({}); - - expect(response).toHaveProperty('config'); - expect(response.config).toHaveProperty('email'); - expect(response.config).toHaveProperty('dns'); - expect(response.config).toHaveProperty('proxy'); - expect(response.config).toHaveProperty('security'); -}); - -tap.test('should handle log retrieval request', async () => { - const logsRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'getRecentLogs' - ); - - const response = await logsRequest.fire({ - limit: 10 - }); - - expect(response).toHaveProperty('logs'); - expect(response).toHaveProperty('total'); - expect(response).toHaveProperty('hasMore'); - expect(response.logs).toBeArray(); -}); - -tap.test('should stop DCRouter', async () => { - await testDcRouter.stop(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.protected-endpoint.ts b/test/test.protected-endpoint.ts deleted file mode 100644 index 69607a4..0000000 --- a/test/test.protected-endpoint.ts +++ /dev/null @@ -1,115 +0,0 @@ -import { expect, tap } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/index.ts'; -import { TypedRequest } from '@api.global/typedrequest'; -import * as interfaces from '../ts_interfaces/index.ts'; - -let testDcRouter: DcRouter; -let adminIdentity: interfaces.data.IIdentity; - -tap.test('should start DCRouter with OpsServer', async () => { - testDcRouter = new DcRouter({ - // Minimal config for testing - }); - - await testDcRouter.start(); - expect(testDcRouter.opsServer).toBeInstanceOf(Object); -}); - -tap.test('should login as admin', async () => { - const loginRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'adminLoginWithUsernameAndPassword' - ); - - const response = await loginRequest.fire({ - username: 'admin', - password: 'admin' - }); - - expect(response).toHaveProperty('identity'); - adminIdentity = response.identity; - console.log('Admin logged in with JWT'); -}); - -tap.test('should allow admin to update configuration', async () => { - const updateRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'updateConfiguration' - ); - - const response = await updateRequest.fire({ - identity: adminIdentity, - section: 'security', - config: { - rateLimit: true, - spamDetection: true - } - }); - - expect(response).toHaveProperty('updated'); - expect(response.updated).toBeTrue(); -}); - -tap.test('should reject configuration update without identity', async () => { - const updateRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'updateConfiguration' - ); - - try { - await updateRequest.fire({ - section: 'security', - config: { - rateLimit: false - } - }); - expect(true).toBeFalse(); // Should not reach here - } catch (error) { - expect(error).toBeTruthy(); - console.log('Successfully rejected request without identity'); - } -}); - -tap.test('should reject configuration update with invalid JWT', async () => { - const updateRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'updateConfiguration' - ); - - try { - await updateRequest.fire({ - identity: { - ...adminIdentity, - jwt: 'invalid.jwt.token' - }, - section: 'security', - config: { - rateLimit: false - } - }); - expect(true).toBeFalse(); // Should not reach here - } catch (error) { - expect(error).toBeTruthy(); - console.log('Successfully rejected request with invalid JWT'); - } -}); - -tap.test('should allow access to public endpoints without auth', async () => { - const healthRequest = new TypedRequest( - 'http://localhost:3000/typedrequest', - 'getHealthStatus' - ); - - // No identity provided - const response = await healthRequest.fire({}); - - expect(response).toHaveProperty('health'); - expect(response.health.healthy).toBeTrue(); - console.log('Public endpoint accessible without auth'); -}); - -tap.test('should stop DCRouter', async () => { - await testDcRouter.stop(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.reputationmonitor.ts b/test/test.reputationmonitor.ts deleted file mode 100644 index 41ab1e1..0000000 --- a/test/test.reputationmonitor.ts +++ /dev/null @@ -1,262 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { SenderReputationMonitor } from '../ts/deliverability/classes.senderreputationmonitor.ts'; - -// Set NODE_ENV to test to prevent loading persisted data -process.env.NODE_ENV = 'test'; - -// Cleanup any temporary test data -const cleanupTestData = () => { - const reputationDataPath = plugins.path.join(paths.dataDir, 'reputation'); - if (plugins.fs.existsSync(reputationDataPath)) { - // Remove the directory recursively using fs instead of smartfile - plugins.fs.rmSync(reputationDataPath, { recursive: true, force: true }); - } -}; - -// Helper to reset the singleton instance between tests -const resetSingleton = () => { - // @ts-ignore - accessing private static field for testing - SenderReputationMonitor.instance = null; - - // Clean up any timeout to prevent race conditions - const activeSendReputationMonitors = Array.from(Object.values(global)) - .filter((item: any) => item && typeof item === 'object' && item._idleTimeout) - .filter((item: any) => - item._onTimeout && - item._onTimeout.toString && - item._onTimeout.toString().includes('updateAllDomainMetrics')); - - // Clear any active timeouts to prevent race conditions - activeSendReputationMonitors.forEach((timer: any) => { - clearTimeout(timer); - }); -}; - -// Before running any tests -tap.test('setup', async () => { - resetSingleton(); - cleanupTestData(); -}); - -// Test initialization of SenderReputationMonitor -tap.test('should initialize SenderReputationMonitor with default settings', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance(); - - expect(reputationMonitor).toBeTruthy(); - // Check if the object has the expected methods - expect(typeof reputationMonitor.recordSendEvent).toEqual('function'); - expect(typeof reputationMonitor.getReputationData).toEqual('function'); - expect(typeof reputationMonitor.getReputationSummary).toEqual('function'); -}); - -// Test initialization with custom settings -tap.test('should initialize SenderReputationMonitor with custom settings', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['example.com', 'test.com'], - updateFrequency: 12 * 60 * 60 * 1000, // 12 hours - alertThresholds: { - minReputationScore: 80, - maxComplaintRate: 0.05 - } - }); - - // Test adding domains - reputationMonitor.addDomain('newdomain.com'); - - // Test retrieving reputation data - const data = reputationMonitor.getReputationData('example.com'); - expect(data).toBeTruthy(); - expect(data.domain).toEqual('example.com'); -}); - -// Test recording and tracking send events -tap.test('should record send events and update metrics', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['example.com'] - }); - - // Record a series of events - reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 }); - reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 }); - reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: true, count: 3 }); - reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: false, count: 2 }); - reputationMonitor.recordSendEvent('example.com', { type: 'complaint', count: 1 }); - - // Check metrics - const metrics = reputationMonitor.getReputationData('example.com'); - - expect(metrics).toBeTruthy(); - expect(metrics.volume.sent).toEqual(100); - expect(metrics.volume.delivered).toEqual(95); - expect(metrics.volume.hardBounces).toEqual(3); - expect(metrics.volume.softBounces).toEqual(2); - expect(metrics.complaints.total).toEqual(1); -}); - -// Test reputation score calculation -tap.test('should calculate reputation scores correctly', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['high.com', 'medium.com', 'low.com'] - }); - - // Record events for different domains - reputationMonitor.recordSendEvent('high.com', { type: 'sent', count: 1000 }); - reputationMonitor.recordSendEvent('high.com', { type: 'delivered', count: 990 }); - reputationMonitor.recordSendEvent('high.com', { type: 'open', count: 500 }); - - reputationMonitor.recordSendEvent('medium.com', { type: 'sent', count: 1000 }); - reputationMonitor.recordSendEvent('medium.com', { type: 'delivered', count: 950 }); - reputationMonitor.recordSendEvent('medium.com', { type: 'open', count: 300 }); - - reputationMonitor.recordSendEvent('low.com', { type: 'sent', count: 1000 }); - reputationMonitor.recordSendEvent('low.com', { type: 'delivered', count: 850 }); - reputationMonitor.recordSendEvent('low.com', { type: 'open', count: 100 }); - - // Get reputation summary - const summary = reputationMonitor.getReputationSummary(); - expect(Array.isArray(summary)).toEqual(true); - expect(summary.length >= 3).toEqual(true); - - // Check that domains are included in the summary - const domains = summary.map(item => item.domain); - expect(domains.includes('high.com')).toEqual(true); - expect(domains.includes('medium.com')).toEqual(true); - expect(domains.includes('low.com')).toEqual(true); -}); - -// Test adding and removing domains -tap.test('should add and remove domains for monitoring', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['example.com'] - }); - - // Add a new domain - reputationMonitor.addDomain('newdomain.com'); - - // Record data for the new domain - reputationMonitor.recordSendEvent('newdomain.com', { type: 'sent', count: 50 }); - - // Check that data was recorded for the new domain - const metrics = reputationMonitor.getReputationData('newdomain.com'); - expect(metrics).toBeTruthy(); - expect(metrics.volume.sent).toEqual(50); - - // Remove a domain - reputationMonitor.removeDomain('newdomain.com'); - - // Check that data is no longer available - const removedMetrics = reputationMonitor.getReputationData('newdomain.com'); - expect(removedMetrics === null).toEqual(true); -}); - -// Test handling open and click events -tap.test('should track engagement metrics correctly', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['example.com'] - }); - - // Record basic sending metrics - reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 1000 }); - reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 950 }); - - // Record engagement events - reputationMonitor.recordSendEvent('example.com', { type: 'open', count: 500 }); - reputationMonitor.recordSendEvent('example.com', { type: 'click', count: 250 }); - - // Check engagement metrics - const metrics = reputationMonitor.getReputationData('example.com'); - expect(metrics).toBeTruthy(); - expect(metrics.engagement.opens).toEqual(500); - expect(metrics.engagement.clicks).toEqual(250); - expect(typeof metrics.engagement.openRate).toEqual('number'); - expect(typeof metrics.engagement.clickRate).toEqual('number'); -}); - -// Test historical data tracking -tap.test('should store historical reputation data', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['example.com'] - }); - - // Record events over multiple days - const today = new Date(); - const todayStr = today.toISOString().split('T')[0]; - - // Record data - reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 1000 }); - reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 950 }); - - // Get metrics data - const metrics = reputationMonitor.getReputationData('example.com'); - - // Check that historical data exists - expect(metrics.historical).toBeTruthy(); - expect(metrics.historical.reputationScores).toBeTruthy(); - - // Check that daily send volume is tracked - expect(metrics.volume.dailySendVolume).toBeTruthy(); - expect(metrics.volume.dailySendVolume[todayStr]).toEqual(1000); -}); - -// Test event recording for different event types -tap.test('should correctly handle different event types', async () => { - resetSingleton(); - const reputationMonitor = SenderReputationMonitor.getInstance({ - enabled: false, // Disable automatic updates to prevent race conditions - domains: ['example.com'] - }); - - // Record different types of events - reputationMonitor.recordSendEvent('example.com', { type: 'sent', count: 100 }); - reputationMonitor.recordSendEvent('example.com', { type: 'delivered', count: 95 }); - reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: true, count: 3 }); - reputationMonitor.recordSendEvent('example.com', { type: 'bounce', hardBounce: false, count: 2 }); - reputationMonitor.recordSendEvent('example.com', { type: 'complaint', receivingDomain: 'gmail.com', count: 1 }); - reputationMonitor.recordSendEvent('example.com', { type: 'open', count: 50 }); - reputationMonitor.recordSendEvent('example.com', { type: 'click', count: 25 }); - - // Check metrics for different event types - const metrics = reputationMonitor.getReputationData('example.com'); - - // Check volume metrics - expect(metrics.volume.sent).toEqual(100); - expect(metrics.volume.delivered).toEqual(95); - expect(metrics.volume.hardBounces).toEqual(3); - expect(metrics.volume.softBounces).toEqual(2); - - // Check complaint metrics - expect(metrics.complaints.total).toEqual(1); - expect(metrics.complaints.topDomains[0].domain).toEqual('gmail.com'); - - // Check engagement metrics - expect(metrics.engagement.opens).toEqual(50); - expect(metrics.engagement.clicks).toEqual(25); -}); - -// After all tests, clean up -tap.test('cleanup', async () => { - resetSingleton(); - cleanupTestData(); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - - -export default tap.start(); \ No newline at end of file diff --git a/test/test.socket-handler-integration.ts b/test/test.socket-handler-integration.ts deleted file mode 100644 index 16b6cd0..0000000 --- a/test/test.socket-handler-integration.ts +++ /dev/null @@ -1,240 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/classes.dcrouter.ts'; -import * as plugins from '../ts/plugins.ts'; - -let dcRouter: DcRouter; - -tap.test('should run both DNS and email with socket-handlers simultaneously', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.integration.test', - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.integration.test', - domains: ['integration.test'], - routes: [], - useSocketHandler: true - }, - smartProxyConfig: { - routes: [] - } - }); - - await dcRouter.start(); - - // Verify both services are running - const dnsServer = (dcRouter as any).dnsServer; - const emailServer = (dcRouter as any).emailServer; - - expect(dnsServer).toBeDefined(); - expect(emailServer).toBeDefined(); - - // Verify SmartProxy has routes for both services - const smartProxy = (dcRouter as any).smartProxy; - const routes = smartProxy?.options?.routes || []; - - // Count DNS routes - const dnsRoutes = routes.filter((route: any) => - route.name?.includes('dns-over-https') - ); - expect(dnsRoutes.length).toEqual(2); - - // Count email routes - const emailRoutes = routes.filter((route: any) => - route.name?.includes('-route') && !route.name?.includes('dns') - ); - expect(emailRoutes.length).toEqual(3); - - // All routes should be socket-handler type - [...dnsRoutes, ...emailRoutes].forEach((route: any) => { - expect(route.action.type).toEqual('socket-handler'); - expect(route.action.socketHandler).toBeDefined(); - }); - - await dcRouter.stop(); -}); - -tap.test('should handle mixed configuration (DNS socket-handler, email traditional)', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.mixed.test', - emailConfig: { - ports: [25, 587], - hostname: 'mail.mixed.test', - domains: ['mixed.test'], - routes: [], - useSocketHandler: false // Traditional mode - }, - smartProxyConfig: { - routes: [] - } - }); - - await dcRouter.start(); - - const smartProxy = (dcRouter as any).smartProxy; - const routes = smartProxy?.options?.routes || []; - - // DNS routes should be socket-handler - const dnsRoutes = routes.filter((route: any) => - route.name?.includes('dns-over-https') - ); - dnsRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('socket-handler'); - }); - - // Email routes should be forward - const emailRoutes = routes.filter((route: any) => - route.name?.includes('-route') && !route.name?.includes('dns') - ); - emailRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('forward'); - expect(route.action.target.port).toBeGreaterThan(10000); // Internal port - }); - - await dcRouter.stop(); -}); - -tap.test('should properly clean up resources on stop', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.cleanup.test', - emailConfig: { - ports: [25], - hostname: 'mail.cleanup.test', - domains: ['cleanup.test'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - // Services should be running - expect((dcRouter as any).dnsServer).toBeDefined(); - expect((dcRouter as any).emailServer).toBeDefined(); - expect((dcRouter as any).smartProxy).toBeDefined(); - - await dcRouter.stop(); - - // After stop, services should still be defined but stopped - // (The stop method doesn't null out the properties, just stops the services) - expect((dcRouter as any).dnsServer).toBeDefined(); - expect((dcRouter as any).emailServer).toBeDefined(); -}); - -tap.test('should handle configuration updates correctly', async () => { - // Start with minimal config - dcRouter = new DcRouter({ - smartProxyConfig: { - routes: [] - } - }); - - await dcRouter.start(); - - // Initially no DNS or email - expect((dcRouter as any).dnsServer).toBeUndefined(); - expect((dcRouter as any).emailServer).toBeUndefined(); - - // Update to add email config - await dcRouter.updateEmailConfig({ - ports: [25], - hostname: 'mail.update.test', - domains: ['update.test'], - routes: [], - useSocketHandler: true - }); - - // Now email should be running - expect((dcRouter as any).emailServer).toBeDefined(); - - await dcRouter.stop(); -}); - -tap.test('performance: socket-handler should not create internal listeners', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.perf.test', - emailConfig: { - ports: [25, 587, 465], - hostname: 'mail.perf.test', - domains: ['perf.test'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - // Get the number of listeners before creating handlers - const eventCounts: { [key: string]: number } = {}; - - // DNS server should not have HTTPS listeners - const dnsServer = (dcRouter as any).dnsServer; - // The DNS server should exist but not bind to HTTPS port - expect(dnsServer).toBeDefined(); - - // Email server should not have any server listeners - const emailServer = (dcRouter as any).emailServer; - expect(emailServer.servers.length).toEqual(0); - - await dcRouter.stop(); -}); - -tap.test('should handle errors gracefully', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.error.test', - emailConfig: { - ports: [25], - hostname: 'mail.error.test', - domains: ['error.test'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - // Test DNS error handling - const dnsHandler = (dcRouter as any).createDnsSocketHandler(); - const errorSocket = new plugins.net.Socket(); - - let errorThrown = false; - try { - // This should handle the error gracefully - await dnsHandler(errorSocket); - } catch (error) { - errorThrown = true; - } - - // Should not throw, should handle gracefully - expect(errorThrown).toBeFalsy(); - - await dcRouter.stop(); -}); - -tap.test('should correctly identify secure connections', async () => { - dcRouter = new DcRouter({ - emailConfig: { - ports: [465], - hostname: 'mail.secure.test', - domains: ['secure.test'], - routes: [], - useSocketHandler: true - } - }); - - await dcRouter.start(); - - // The email socket handler for port 465 should handle TLS - const handler = (dcRouter as any).createMailSocketHandler(465); - expect(handler).toBeDefined(); - - // Port 465 requires immediate TLS, which is handled in the socket handler - // This is different from ports 25/587 which use STARTTLS - - await dcRouter.stop(); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.socket-handler-unit.ts b/test/test.socket-handler-unit.ts deleted file mode 100644 index caf73de..0000000 --- a/test/test.socket-handler-unit.ts +++ /dev/null @@ -1,198 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import { DcRouter } from '../ts/classes.dcrouter.ts'; - -/** - * Unit tests for socket-handler functionality - * These tests focus on the configuration and route generation logic - * without actually starting services on real ports - */ - -let dcRouter: DcRouter; - -tap.test('DNS route generation with dnsDomain', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.unit.test' - }); - - // Test the route generation directly - const dnsRoutes = (dcRouter as any).generateDnsRoutes(); - - expect(dnsRoutes).toBeDefined(); - expect(dnsRoutes.length).toEqual(2); - - // Check /dns-query route - const dnsQueryRoute = dnsRoutes[0]; - expect(dnsQueryRoute.name).toEqual('dns-over-https-dns-query'); - expect(dnsQueryRoute.match.ports).toEqual([443]); - expect(dnsQueryRoute.match.domains).toEqual(['dns.unit.test']); - expect(dnsQueryRoute.match.path).toEqual('/dns-query'); - expect(dnsQueryRoute.action.type).toEqual('socket-handler'); - expect(dnsQueryRoute.action.socketHandler).toBeDefined(); - - // Check /resolve route - const resolveRoute = dnsRoutes[1]; - expect(resolveRoute.name).toEqual('dns-over-https-resolve'); - expect(resolveRoute.match.ports).toEqual([443]); - expect(resolveRoute.match.domains).toEqual(['dns.unit.test']); - expect(resolveRoute.match.path).toEqual('/resolve'); - expect(resolveRoute.action.type).toEqual('socket-handler'); - expect(resolveRoute.action.socketHandler).toBeDefined(); -}); - -tap.test('DNS route generation without dnsDomain', async () => { - dcRouter = new DcRouter({ - // No dnsDomain set - }); - - const dnsRoutes = (dcRouter as any).generateDnsRoutes(); - - expect(dnsRoutes).toBeDefined(); - expect(dnsRoutes.length).toEqual(0); // No routes generated -}); - -tap.test('Email route generation with socket-handler', async () => { - const emailConfig = { - ports: [25, 587, 465], - hostname: 'mail.unit.test', - domains: ['unit.test'], - routes: [], - useSocketHandler: true - }; - - dcRouter = new DcRouter({ emailConfig }); - - const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig); - - expect(emailRoutes).toBeDefined(); - expect(emailRoutes.length).toEqual(3); - - // Check all routes use socket-handler - emailRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('socket-handler'); - expect(route.action.socketHandler).toBeDefined(); - expect(typeof route.action.socketHandler).toEqual('function'); - }); - - // Check specific ports - const port25Route = emailRoutes.find((r: any) => r.match.ports[0] === 25); - expect(port25Route.name).toEqual('smtp-route'); - - const port587Route = emailRoutes.find((r: any) => r.match.ports[0] === 587); - expect(port587Route.name).toEqual('submission-route'); - - const port465Route = emailRoutes.find((r: any) => r.match.ports[0] === 465); - expect(port465Route.name).toEqual('smtps-route'); -}); - -tap.test('Email route generation with traditional forwarding', async () => { - const emailConfig = { - ports: [25, 587], - hostname: 'mail.unit.test', - domains: ['unit.test'], - routes: [], - useSocketHandler: false // Traditional mode - }; - - dcRouter = new DcRouter({ emailConfig }); - - const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig); - - expect(emailRoutes).toBeDefined(); - expect(emailRoutes.length).toEqual(2); - - // Check all routes use forward action - emailRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('forward'); - expect(route.action.target).toBeDefined(); - expect(route.action.target.host).toEqual('localhost'); - expect(route.action.target.port).toBeGreaterThan(10000); // Internal port - }); -}); - -tap.test('Email TLS modes are set correctly', async () => { - const emailConfig = { - ports: [25, 465], - hostname: 'mail.unit.test', - domains: ['unit.test'], - routes: [], - useSocketHandler: false - }; - - dcRouter = new DcRouter({ emailConfig }); - - const emailRoutes = (dcRouter as any).generateEmailRoutes(emailConfig); - - // Port 25 should use passthrough (STARTTLS) - const port25Route = emailRoutes.find((r: any) => r.match.ports[0] === 25); - expect(port25Route.action.tls.mode).toEqual('passthrough'); - - // Port 465 should use terminate (implicit TLS) - const port465Route = emailRoutes.find((r: any) => r.match.ports[0] === 465); - expect(port465Route.action.tls.mode).toEqual('terminate'); - expect(port465Route.action.tls.certificate).toEqual('auto'); -}); - -tap.test('Combined DNS and email configuration', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.combined.test', - emailConfig: { - ports: [25], - hostname: 'mail.combined.test', - domains: ['combined.test'], - routes: [], - useSocketHandler: true - } - }); - - // Generate both types of routes - const dnsRoutes = (dcRouter as any).generateDnsRoutes(); - const emailRoutes = (dcRouter as any).generateEmailRoutes(dcRouter.options.emailConfig); - - // Check DNS routes - expect(dnsRoutes.length).toEqual(2); - dnsRoutes.forEach((route: any) => { - expect(route.action.type).toEqual('socket-handler'); - expect(route.match.domains).toEqual(['dns.combined.test']); - }); - - // Check email routes - expect(emailRoutes.length).toEqual(1); - expect(emailRoutes[0].action.type).toEqual('socket-handler'); - expect(emailRoutes[0].match.ports).toEqual([25]); -}); - -tap.test('Socket handler functions are created correctly', async () => { - dcRouter = new DcRouter({ - dnsDomain: 'dns.handler.test', - emailConfig: { - ports: [25, 465], - hostname: 'mail.handler.test', - domains: ['handler.test'], - routes: [], - useSocketHandler: true - } - }); - - // Test DNS socket handler creation - const dnsHandler = (dcRouter as any).createDnsSocketHandler(); - expect(dnsHandler).toBeDefined(); - expect(typeof dnsHandler).toEqual('function'); - - // Test email socket handler creation for different ports - const smtp25Handler = (dcRouter as any).createMailSocketHandler(25); - expect(smtp25Handler).toBeDefined(); - expect(typeof smtp25Handler).toEqual('function'); - - const smtp465Handler = (dcRouter as any).createMailSocketHandler(465); - expect(smtp465Handler).toBeDefined(); - expect(typeof smtp465Handler).toEqual('function'); - - // Handlers should be different functions - expect(smtp25Handler).not.toEqual(smtp465Handler); -}); - -tap.test('stop', async () => { - await tap.stopForcefully(); -}); - -export default tap.start(); \ No newline at end of file diff --git a/test/test.storagemanager.ts b/test/test.storagemanager.ts deleted file mode 100644 index 2481878..0000000 --- a/test/test.storagemanager.ts +++ /dev/null @@ -1,289 +0,0 @@ -import { tap, expect } from '@git.zone/tstest/tapbundle'; -import * as plugins from '../ts/plugins.ts'; -import * as paths from '../ts/paths.ts'; -import { StorageManager } from '../ts/storage/classes.storagemanager.ts'; -import { promises as fs } from 'fs'; -import * as path from 'path'; - -// Test data -const testData = { - string: 'Hello, World!', - json: { name: 'test', value: 42, nested: { data: true } }, - largeString: 'x'.repeat(10000) -}; - -tap.test('Storage Manager - Memory Backend', async () => { - // Create StorageManager without config (defaults to memory) - const storage = new StorageManager(); - - // Test basic get/set - await storage.set('/test/key', testData.string); - const value = await storage.get('/test/key'); - expect(value).toEqual(testData.string); - - // Test JSON helpers - await storage.setJSON('/test/json', testData.json); - const jsonValue = await storage.getJSON('/test/json'); - expect(jsonValue).toEqual(testData.json); - - // Test exists - expect(await storage.exists('/test/key')).toEqual(true); - expect(await storage.exists('/nonexistent')).toEqual(false); - - // Test delete - await storage.delete('/test/key'); - expect(await storage.exists('/test/key')).toEqual(false); - - // Test list - await storage.set('/items/1', 'one'); - await storage.set('/items/2', 'two'); - await storage.set('/other/3', 'three'); - - const items = await storage.list('/items'); - expect(items.length).toEqual(2); - expect(items).toContain('/items/1'); - expect(items).toContain('/items/2'); - - // Verify memory backend - expect(storage.getBackend()).toEqual('memory'); -}); - -tap.test('Storage Manager - Filesystem Backend', async () => { - const testDir = path.join(paths.dataDir, '.test-storage'); - - // Clean up test directory if it exists - try { - await fs.rm(testDir, { recursive: true, force: true }); - } catch {} - - // Create StorageManager with filesystem path - const storage = new StorageManager({ fsPath: testDir }); - - // Test basic operations - await storage.set('/test/file', testData.string); - const value = await storage.get('/test/file'); - expect(value).toEqual(testData.string); - - // Verify file exists on disk - const filePath = path.join(testDir, 'test', 'file'); - const fileExists = await fs.access(filePath).then(() => true).catch(() => false); - expect(fileExists).toEqual(true); - - // Test atomic writes (temp file should not exist) - const tempPath = filePath + '.tmp'; - const tempExists = await fs.access(tempPath).then(() => true).catch(() => false); - expect(tempExists).toEqual(false); - - // Test nested paths - await storage.set('/deeply/nested/path/to/file', testData.largeString); - const nestedValue = await storage.get('/deeply/nested/path/to/file'); - expect(nestedValue).toEqual(testData.largeString); - - // Test list with filesystem - await storage.set('/fs/items/a', 'alpha'); - await storage.set('/fs/items/b', 'beta'); - await storage.set('/fs/other/c', 'gamma'); - - // Filesystem backend now properly supports list - const fsItems = await storage.list('/fs/items'); - expect(fsItems.length).toEqual(2); // Should find both items - - // Clean up - await fs.rm(testDir, { recursive: true, force: true }); -}); - -tap.test('Storage Manager - Custom Function Backend', async () => { - // Create in-memory storage for custom functions - const customStore = new Map(); - - const storage = new StorageManager({ - readFunction: async (key: string) => { - return customStore.get(key) || null; - }, - writeFunction: async (key: string, value: string) => { - customStore.set(key, value); - } - }); - - // Test basic operations - await storage.set('/custom/key', testData.string); - expect(customStore.has('/custom/key')).toEqual(true); - - const value = await storage.get('/custom/key'); - expect(value).toEqual(testData.string); - - // Test that delete sets empty value (as per implementation) - await storage.delete('/custom/key'); - expect(customStore.get('/custom/key')).toEqual(''); - - // Verify custom backend (filesystem is implemented as custom backend internally) - expect(storage.getBackend()).toEqual('custom'); -}); - -tap.test('Storage Manager - Key Validation', async () => { - const storage = new StorageManager(); - - // Test key normalization - await storage.set('test/key', 'value1'); // Missing leading slash - const value1 = await storage.get('/test/key'); - expect(value1).toEqual('value1'); - - // Test dangerous path elements are removed - await storage.set('/test/../danger/key', 'value2'); - const value2 = await storage.get('/test/danger/key'); // .. is removed, not the whole path segment - expect(value2).toEqual('value2'); - - // Test multiple slashes are normalized - await storage.set('/test///multiple////slashes', 'value3'); - const value3 = await storage.get('/test/multiple/slashes'); - expect(value3).toEqual('value3'); - - // Test invalid keys throw errors - let emptyKeyError: Error | null = null; - try { - await storage.set('', 'value'); - } catch (error) { - emptyKeyError = error as Error; - } - expect(emptyKeyError).toBeTruthy(); - expect(emptyKeyError?.message).toEqual('Storage key must be a non-empty string'); - - let nullKeyError: Error | null = null; - try { - await storage.set(null as any, 'value'); - } catch (error) { - nullKeyError = error as Error; - } - expect(nullKeyError).toBeTruthy(); - expect(nullKeyError?.message).toEqual('Storage key must be a non-empty string'); -}); - -tap.test('Storage Manager - Concurrent Access', async () => { - const storage = new StorageManager(); - const promises: Promise[] = []; - - // Simulate concurrent writes - for (let i = 0; i < 100; i++) { - promises.push(storage.set(`/concurrent/key${i}`, `value${i}`)); - } - - await Promise.all(promises); - - // Verify all writes succeeded - for (let i = 0; i < 100; i++) { - const value = await storage.get(`/concurrent/key${i}`); - expect(value).toEqual(`value${i}`); - } - - // Test concurrent reads - const readPromises: Promise[] = []; - for (let i = 0; i < 100; i++) { - readPromises.push(storage.get(`/concurrent/key${i}`)); - } - - const results = await Promise.all(readPromises); - for (let i = 0; i < 100; i++) { - expect(results[i]).toEqual(`value${i}`); - } -}); - -tap.test('Storage Manager - Backend Priority', async () => { - const testDir = path.join(paths.dataDir, '.test-storage-priority'); - - // Test that custom functions take priority over fsPath - let warningLogged = false; - const originalWarn = console.warn; - console.warn = (message: string) => { - if (message.includes('Using custom read/write functions')) { - warningLogged = true; - } - }; - - const storage = new StorageManager({ - fsPath: testDir, - readFunction: async () => 'custom-value', - writeFunction: async () => {} - }); - - console.warn = originalWarn; - - expect(warningLogged).toEqual(true); - expect(storage.getBackend()).toEqual('custom'); // Custom functions take priority - - // Clean up - try { - await fs.rm(testDir, { recursive: true, force: true }); - } catch {} -}); - -tap.test('Storage Manager - Error Handling', async () => { - // Test filesystem errors - const storage = new StorageManager({ - readFunction: async () => { - throw new Error('Read error'); - }, - writeFunction: async () => { - throw new Error('Write error'); - } - }); - - // Read errors should return null - const value = await storage.get('/error/key'); - expect(value).toEqual(null); - - // Write errors should propagate - let writeError: Error | null = null; - try { - await storage.set('/error/key', 'value'); - } catch (error) { - writeError = error as Error; - } - expect(writeError).toBeTruthy(); - expect(writeError?.message).toEqual('Write error'); - - // Test JSON parse errors - const jsonStorage = new StorageManager({ - readFunction: async () => 'invalid json', - writeFunction: async () => {} - }); - - // Test JSON parse errors - let jsonError: Error | null = null; - try { - await jsonStorage.getJSON('/invalid/json'); - } catch (error) { - jsonError = error as Error; - } - expect(jsonError).toBeTruthy(); - expect(jsonError?.message).toContain('JSON'); -}); - -tap.test('Storage Manager - List Operations', async () => { - const storage = new StorageManager(); - - // Populate storage with hierarchical data - await storage.set('/app/config/database', 'db-config'); - await storage.set('/app/config/cache', 'cache-config'); - await storage.set('/app/data/users/1', 'user1'); - await storage.set('/app/data/users/2', 'user2'); - await storage.set('/app/logs/error.log', 'errors'); - - // List root - const rootItems = await storage.list('/'); - expect(rootItems.length).toBeGreaterThanOrEqual(5); - - // List specific paths - const configItems = await storage.list('/app/config'); - expect(configItems.length).toEqual(2); - expect(configItems).toContain('/app/config/database'); - expect(configItems).toContain('/app/config/cache'); - - const userItems = await storage.list('/app/data/users'); - expect(userItems.length).toEqual(2); - - // List non-existent path - const emptyList = await storage.list('/nonexistent/path'); - expect(emptyList.length).toEqual(0); -}); - -export default tap.start(); \ No newline at end of file