import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { existsSync } from 'node:fs'; import { unlink } from 'node:fs/promises'; import { type DbClient, createDbClient } from '../db/client'; import { capabilities, moduleConfigs, modules, secrets, systemConfig } from '../db/schema'; import type { NewModule } from '../db/schema'; import { CrossModuleDataManager } from './cross-module-data-manager'; describe('CrossModuleDataManager', () => { let db: DbClient; let manager: CrossModuleDataManager; let testDbPath: string; beforeEach(async () => { testDbPath = `./test-cross-module-${Date.now()}-${Math.random()}.db`; db = createDbClient({ path: testDbPath }); // Tables are automatically created by auto-migration in createDbClient() // Insert test module const testModule: NewModule = { id: 'test-module', name: 'Test Module', version: '1.0.0', sourcePath: '/test', manifestData: {}, }; db.insert(modules).values(testModule).run(); // Insert system config db.insert(systemConfig) .values({ key: 'dns.primary', value: '1.1.1.1', }) .run(); // Create and initialize manager manager = new CrossModuleDataManager(db); await manager.initialize(); }); afterEach(async () => { db.$client.close(); if (existsSync(testDbPath)) { await unlink(testDbPath); } const walPath = `${testDbPath}-wal`; const shmPath = `${testDbPath}-shm`; if (existsSync(walPath)) { await unlink(walPath); } if (existsSync(shmPath)) { await unlink(shmPath); } }); describe('Configuration Storage', () => { test('should store and retrieve string config', async () => { await manager.storeConfigData('test-module', 'hostname', 'example.com', false); const value = manager.getConfigData('test-module', 'hostname'); expect(value).toBe('example.com'); }); test('should store and retrieve number config', async () => { await manager.storeConfigData('test-module', 'port', 8080, false); const value = manager.getConfigData('test-module', 'port'); expect(value).toBe(8080); }); test('should store and retrieve boolean config', async () => { await manager.storeConfigData('test-module', 'enabled', true, false); const value = manager.getConfigData('test-module', 'enabled'); expect(value).toBe(true); }); test('should store and retrieve array config', async () => { const arrayValue = ['item1', 'item2', 'item3']; await manager.storeConfigData('test-module', 'items', arrayValue, false); const value = manager.getConfigData('test-module', 'items'); expect(value).toEqual(arrayValue); }); test('should store and retrieve object config', async () => { const objectValue = { host: 'localhost', port: 5432, database: 'test' }; await manager.storeConfigData('test-module', 'db_config', objectValue, false); const value = manager.getConfigData('test-module', 'db_config'); expect(value).toEqual(objectValue); }); test('should update existing config', async () => { await manager.storeConfigData('test-module', 'hostname', 'old.example.com', false); await manager.storeConfigData('test-module', 'hostname', 'new.example.com', false); const value = manager.getConfigData('test-module', 'hostname'); expect(value).toBe('new.example.com'); // Should only have one record const allConfigs = db.select().from(moduleConfigs).all(); expect(allConfigs).toHaveLength(1); }); test('should return null for non-existent config', () => { const value = manager.getConfigData('test-module', 'nonexistent'); expect(value).toBeNull(); }); }); describe('Secret Storage', () => { test('should store and retrieve secret', async () => { const secretValue = 'super-secret-password'; await manager.storeSecret('test-module', 'db_password', secretValue); const retrieved = manager.getSecret('test-module', 'db_password'); expect(retrieved).toBe(secretValue); }); test('should encrypt secrets in database', async () => { const secretValue = 'my-secret'; await manager.storeSecret('test-module', 'api_key', secretValue); // Read directly from database const secret = db.select().from(secrets).get(); expect(secret).toBeDefined(); // Encrypted value should not equal plaintext expect(secret?.encryptedValue).not.toBe(secretValue); // Should have IV and auth tag expect(secret?.iv).toBeDefined(); expect(secret?.authTag).toBeDefined(); }); test('should update existing secret', async () => { await manager.storeSecret('test-module', 'password', 'old-password'); await manager.storeSecret('test-module', 'password', 'new-password'); const retrieved = manager.getSecret('test-module', 'password'); expect(retrieved).toBe('new-password'); // Should only have one record const allSecrets = db.select().from(secrets).all(); expect(allSecrets).toHaveLength(1); }); test('should return null for non-existent secret', () => { const value = manager.getSecret('test-module', 'nonexistent'); expect(value).toBeNull(); }); test('should get all secrets for a module', async () => { await manager.storeSecret('test-module', 'password', 'pass123'); await manager.storeSecret('test-module', 'api_key', 'key456'); await manager.storeSecret('test-module', 'token', 'token789'); const allSecrets = manager.getAllSecrets('test-module'); expect(allSecrets).toEqual({ password: 'pass123', api_key: 'key456', token: 'token789', }); }); test('should store secret via storeConfigData with isSecret=true', async () => { await manager.storeConfigData('test-module', 'secret_key', 'my-secret', true); const retrieved = manager.getSecret('test-module', 'secret_key'); expect(retrieved).toBe('my-secret'); }); }); describe('Capability Lookup', () => { test('should find capability provider', () => { // Insert dns-external module db.insert(modules) .values({ id: 'dns-external', name: 'DNS External', version: '1.0.0', sourcePath: '/test/dns-external', manifestData: {}, }) .run(); // Insert capability db.insert(capabilities) .values({ moduleId: 'dns-external', capabilityName: 'dns_external', version: '1.0.0', data: { server: { ip: { primary: '10.0.10.10' }, port: 53, }, }, }) .run(); const capability = manager.findCapabilityProvider('dns_external'); expect(capability).toBeDefined(); expect(capability?.moduleId).toBe('dns-external'); expect(capability?.capabilityName).toBe('dns_external'); expect(capability?.version).toBe('1.0.0'); expect(capability?.data).toEqual({ server: { ip: { primary: '10.0.10.10' }, port: 53, }, }); }); test('should return null for non-existent capability', () => { const capability = manager.findCapabilityProvider('nonexistent'); expect(capability).toBeNull(); }); }); describe('Parameter Resolution', () => { beforeEach(() => { // Add dns-external module for capability tests db.insert(modules) .values({ id: 'dns-external', name: 'DNS External', version: '1.0.0', sourcePath: '/test/dns-external', manifestData: {}, }) .run(); // Add module config for $self: resolution db.insert(moduleConfigs) .values([ { moduleId: 'test-module', key: 'domain', value: 'example.com', valueJson: null }, { moduleId: 'test-module', key: 'port', value: '8080', valueJson: null }, ]) .run(); // Add capability for $capability: resolution db.insert(capabilities) .values({ moduleId: 'dns-external', capabilityName: 'dns_external', version: '1.0.0', data: { server: { ip: { primary: '10.0.10.10' }, }, }, }) .run(); }); test('should resolve $self: variables', async () => { const params = { url: 'https://$self:domain', }; const resolved = await manager.resolveParameters('test-module', params); expect(resolved).toEqual({ url: 'https://example.com', }); }); test('should resolve $system: variables', async () => { const params = { dns: '$system:dns.primary', }; const resolved = await manager.resolveParameters('test-module', params); expect(resolved).toEqual({ dns: '1.1.1.1', }); }); test('should resolve $capability: variables', async () => { const params = { dns_server: '$capability:dns_external.server.ip.primary', }; const resolved = await manager.resolveParameters('test-module', params); expect(resolved).toEqual({ dns_server: '10.0.10.10', }); }); test('should resolve variables in arrays', async () => { const params = { urls: ['https://$self:domain', 'http://$self:domain:$self:port'], }; const resolved = await manager.resolveParameters('test-module', params); expect(resolved).toEqual({ urls: ['https://example.com', 'http://example.com:8080'], }); }); test('should resolve nested objects', async () => { const params = { server: { host: '$self:domain', port: 443, }, }; const resolved = await manager.resolveParameters('test-module', params); expect(resolved).toEqual({ server: { host: 'example.com', port: 443, }, }); }); test('should throw error for unresolvable variables', async () => { const params = { invalid: '$self:nonexistent', }; await expect(manager.resolveParameters('test-module', params)).rejects.toThrow( 'Failed to resolve parameter', ); }); }); describe('Secret Generation', () => { test('should generate TSIG key', () => { const key = manager.generateTSIGKey(); expect(typeof key).toBe('string'); expect(key.length).toBeGreaterThan(0); // Should be valid base64 expect(() => atob(key)).not.toThrow(); }); test('should generate different TSIG keys', () => { const key1 = manager.generateTSIGKey(); const key2 = manager.generateTSIGKey(); expect(key1).not.toBe(key2); }); test('should generate hex secret', () => { const secret = manager.generateSecret(32, 'hex'); expect(typeof secret).toBe('string'); expect(secret.length).toBe(64); // 32 bytes = 64 hex chars expect(/^[0-9a-f]+$/.test(secret)).toBe(true); }); test('should generate base64 secret', () => { const secret = manager.generateSecret(32, 'base64'); expect(typeof secret).toBe('string'); expect(secret.length).toBeGreaterThan(0); // Should be valid base64 expect(() => atob(secret)).not.toThrow(); }); test('should generate different secrets', () => { const secret1 = manager.generateSecret(); const secret2 = manager.generateSecret(); expect(secret1).not.toBe(secret2); }); }); describe('Initialization', () => { test('should require initialization before use', async () => { const uninitializedManager = new CrossModuleDataManager(db); // Should throw when trying to use secrets without initialization await expect(uninitializedManager.storeSecret('test-module', 'key', 'value')).rejects.toThrow( 'not initialized', ); }); test('should work after initialization', async () => { const newManager = new CrossModuleDataManager(db); await newManager.initialize(); // Should work now await newManager.storeSecret('test-module', 'key', 'value'); // Verify secret was stored const retrieved = newManager.getSecret('test-module', 'key'); expect(retrieved).toBe('value'); }); }); });