import { tap, expect } from '@git.zone/tstest/tapbundle'; import * as plugins from '../ts/plugins.js'; import * as paths from '../ts/paths.js'; import { StorageManager } from '../ts/storage/classes.storagemanager.js'; import { DKIMCreator } from '../ts/mail/security/classes.dkimcreator.js'; import { BounceManager } from '../ts/mail/core/classes.bouncemanager.js'; import { EmailRouter } from '../ts/mail/routing/classes.email.router.js'; import type { IEmailRoute } from '../ts/mail/routing/interfaces.js'; 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();