dcrouter/test/test.integration.storage.ts

313 lines
11 KiB
TypeScript
Raw Normal View History

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<string, string>();
{
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();