/** * Tests for capability secret_ref resolution * Verifies that capability secrets can reference provider module secrets */ import { afterEach, describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { createDbClient } from '@/db/client'; import { encryptSecret } from '@/secrets/encryption'; import { getOrCreateMasterKey } from '@/secrets/master-key'; import { validateCapabilitySecrets } from './secret-validation'; import { getCapabilitySecret } from './secrets'; let testDirs: string[] = []; afterEach(() => { // Clean up all test databases for (const dir of testDirs) { try { rmSync(dir, { recursive: true, force: true }); } catch { // Ignore errors } } testDirs = []; }); describe('Capability secret_ref resolution', () => { test('should resolve secret_ref to provider module secret', async () => { // Create unique test database const testDir = mkdtempSync(join(tmpdir(), 'test-secret-ref-')); testDirs.push(testDir); const testDbPath = join(testDir, 'test.db'); const db = createDbClient({ path: testDbPath }); const masterKey = await getOrCreateMasterKey(); // Create provider module (dns-external) db.$client .prepare( `INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)`, ) .run( 'dns-external', 'DNS External', '1.0.0', '/tmp/modules/dns-external', 'CONFIGURED', JSON.stringify({ id: 'dns-external', name: 'DNS External', version: '1.0.0', provides: { capabilities: [ { name: 'dns_external', version: '1.0.0', data: { server: { ip: { primary: '203.0.113.42' } }, }, secrets: [ { name: 'tsig', type: 'string', description: 'TSIG secret for DNS updates', readable_by: ['public_web'], secret_ref: '$secret:knot_ddns_tsig_secret', }, ], }, ], }, }), ); // Store module secret that capability references const secretValue = 'test-tsig-secret-base64=='; const encrypted = encryptSecret(secretValue, masterKey); db.$client .prepare( `INSERT INTO secrets (module_id, name, encrypted_value, iv, auth_tag) VALUES (?, ?, ?, ?, ?)`, ) .run( 'dns-external', 'knot_ddns_tsig_secret', encrypted.encryptedValue, encrypted.iv, encrypted.authTag, ); // Register capability const capabilityId = db.$client .prepare( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?) RETURNING id`, ) .get('dns-external', 'dns_external', '1.0.0', JSON.stringify({})) as { id: number }; // Register capability secret metadata (no encrypted value - uses secret_ref) db.$client .prepare( `INSERT INTO capability_secrets (capability_id, name, description) VALUES (?, ?, ?)`, ) .run(capabilityId.id, 'tsig', 'TSIG secret for DNS updates'); // A configured secret_ref satisfies generation validation without // duplicating the secret into capability_secrets. const validation = await validateCapabilitySecrets('dns-external', db.$client); expect(validation).toEqual({ success: true }); // Test: Resolve capability secret via secret_ref const result = await getCapabilitySecret('dns_external', 'tsig', db.$client); expect(result).toBe(secretValue); }); test('should throw error if secret_ref references non-existent module secret', async () => { // Create unique test database const testDir = mkdtempSync(join(tmpdir(), 'test-secret-ref-')); testDirs.push(testDir); const testDbPath = join(testDir, 'test.db'); const db = createDbClient({ path: testDbPath }); // Create provider module with secret_ref to non-existent secret db.$client .prepare( `INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)`, ) .run( 'dns-external', 'DNS External', '1.0.0', '/tmp/modules/dns-external', 'CONFIGURED', JSON.stringify({ id: 'dns-external', name: 'DNS External', version: '1.0.0', provides: { capabilities: [ { name: 'dns_external', version: '1.0.0', data: {}, secrets: [ { name: 'tsig', secret_ref: '$secret:nonexistent_secret', }, ], }, ], }, }), ); // Register capability const capabilityId = db.$client .prepare( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?) RETURNING id`, ) .get('dns-external', 'dns_external', '1.0.0', JSON.stringify({})) as { id: number }; // Register capability secret metadata db.$client .prepare( `INSERT INTO capability_secrets (capability_id, name) VALUES (?, ?)`, ) .run(capabilityId.id, 'tsig'); const validation = await validateCapabilitySecrets('dns-external', db.$client); expect(validation.success).toBe(false); expect(validation.missingSecrets).toEqual([ { capabilityId: capabilityId.id, capabilityName: 'dns_external', secretName: 'tsig', description: null, }, ]); // Test: Should throw error about missing module secret await expect(getCapabilitySecret('dns_external', 'tsig', db.$client)).rejects.toThrow( "Module secret 'nonexistent_secret' not found", ); }); test('should fall back to capability_secrets table when no secret_ref', async () => { // Create unique test database const testDir = mkdtempSync(join(tmpdir(), 'test-secret-ref-')); testDirs.push(testDir); const testDbPath = join(testDir, 'test.db'); const db = createDbClient({ path: testDbPath }); const masterKey = await getOrCreateMasterKey(); // Create provider module WITHOUT secret_ref db.$client .prepare( `INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)`, ) .run( 'test-module', 'Test Module', '1.0.0', '/tmp/modules/test', 'CONFIGURED', JSON.stringify({ id: 'test-module', name: 'Test Module', version: '1.0.0', provides: { capabilities: [ { name: 'test_capability', version: '1.0.0', data: {}, secrets: [ { name: 'api_key', type: 'string', // No secret_ref - direct storage }, ], }, ], }, }), ); // Register capability const capabilityId = db.$client .prepare( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?) RETURNING id`, ) .get('test-module', 'test_capability', '1.0.0', JSON.stringify({})) as { id: number }; // Store secret directly in capability_secrets table const secretValue = 'direct-api-key-value'; const encrypted = encryptSecret(secretValue, masterKey); db.$client .prepare( `INSERT INTO capability_secrets (capability_id, name, encrypted_value, iv, auth_tag) VALUES (?, ?, ?, ?, ?)`, ) .run(capabilityId.id, 'api_key', encrypted.encryptedValue, encrypted.iv, encrypted.authTag); const validation = await validateCapabilitySecrets('test-module', db.$client); expect(validation).toEqual({ success: true }); // Test: Should read from capability_secrets table const result = await getCapabilitySecret('test_capability', 'api_key', db.$client); expect(result).toBe(secretValue); }); test('should throw error if capability_secrets entry has no encrypted values', async () => { // Create unique test database const testDir = mkdtempSync(join(tmpdir(), 'test-secret-ref-')); testDirs.push(testDir); const testDbPath = join(testDir, 'test.db'); const db = createDbClient({ path: testDbPath }); // Create provider module without secret_ref db.$client .prepare( `INSERT INTO modules (id, name, version, source_path, state, manifest_data) VALUES (?, ?, ?, ?, ?, ?)`, ) .run( 'test-module', 'Test Module', '1.0.0', '/tmp/modules/test', 'CONFIGURED', JSON.stringify({ id: 'test-module', name: 'Test Module', version: '1.0.0', provides: { capabilities: [ { name: 'test_capability', version: '1.0.0', data: {}, secrets: [ { name: 'api_key', type: 'string', }, ], }, ], }, }), ); // Register capability const capabilityId = db.$client .prepare( `INSERT INTO capabilities (module_id, capability_name, version, data) VALUES (?, ?, ?, ?) RETURNING id`, ) .get('test-module', 'test_capability', '1.0.0', JSON.stringify({})) as { id: number }; // Insert capability_secrets metadata only (no encrypted values) db.$client .prepare( `INSERT INTO capability_secrets (capability_id, name) VALUES (?, ?)`, ) .run(capabilityId.id, 'api_key'); const validation = await validateCapabilitySecrets('test-module', db.$client); expect(validation.success).toBe(false); expect(validation.missingSecrets?.map((secret) => secret.secretName)).toEqual(['api_key']); // Test: Should throw helpful error await expect(getCapabilitySecret('test_capability', 'api_key', db.$client)).rejects.toThrow( "Secret 'api_key' in capability 'test_capability' has not been set", ); }); });