import { afterEach, beforeEach, describe, expect, test } from 'bun:test'; import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { type DbClient, getDb } from '../db/client'; import { modules } from '../db/schema'; import { resetTestDbPath } from '../test-utils/db-path'; import { type SecretsSchema, getAllSecretMetadata, getSecretMetadata, loadSecretsSchema, validateDerivationGraph, } from './secret-schema-loader'; describe('secret-schema-loader', () => { let tempDir: string; let testDb: DbClient; beforeEach(() => { // Create temporary directory for test module tempDir = mkdtempSync(join(tmpdir(), 'celilo-test-')); // Set up test database process.env.CELILO_DB_PATH = join(tempDir, 'test.db'); testDb = getDb(); // Create test module in database testDb .insert(modules) .values({ id: 'test-module', name: 'Test Module', sourcePath: tempDir, version: '1.0.0', manifestData: {}, }) .run(); }); afterEach(() => { // Clean up temp directory rmSync(tempDir, { recursive: true, force: true }); resetTestDbPath(); }); describe('loadSecretsSchema', () => { test('loads valid secrets schema', async () => { // Create schema directory and file const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); const schema = { type: 'object', properties: { api_key: { type: 'string', source: 'user_provided', description: 'API key for service', }, tsig_secret: { type: 'string', source: 'generated', format: 'tsig-key', }, }, }; writeFileSync(join(schemaDir, 'secrets.json'), JSON.stringify(schema, null, 2)); const result = await loadSecretsSchema('test-module', testDb); expect(result).not.toBeNull(); expect(result?.properties.api_key).toBeDefined(); expect(result?.properties.api_key.source).toBe('user_provided'); expect(result?.properties.tsig_secret.source).toBe('generated'); }); test('returns null when schema file does not exist', async () => { const result = await loadSecretsSchema('test-module', testDb); expect(result).toBeNull(); }); test('returns null for invalid schema structure', async () => { const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); // Invalid schema (missing properties) const schema = { type: 'object', }; writeFileSync(join(schemaDir, 'secrets.json'), JSON.stringify(schema, null, 2)); const result = await loadSecretsSchema('test-module', testDb); expect(result).toBeNull(); }); test('returns null for malformed JSON', async () => { const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); writeFileSync(join(schemaDir, 'secrets.json'), 'not valid json{'); const result = await loadSecretsSchema('test-module', testDb); expect(result).toBeNull(); }); }); describe('getSecretMetadata', () => { test('returns metadata for specific secret', async () => { const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); const schema = { type: 'object', properties: { wireguard_private_key: { type: 'string', source: 'generated', format: 'wireguard-key', length: 32, title: 'WireGuard Private Key', description: 'Private key for WireGuard tunnel', }, }, }; writeFileSync(join(schemaDir, 'secrets.json'), JSON.stringify(schema, null, 2)); const metadata = await getSecretMetadata('test-module', 'wireguard_private_key', testDb); expect(metadata).not.toBeNull(); expect(metadata?.name).toBe('wireguard_private_key'); expect(metadata?.source).toBe('generated'); expect(metadata?.format).toBe('wireguard-key'); expect(metadata?.length).toBe(32); expect(metadata?.title).toBe('WireGuard Private Key'); }); test('returns null for non-existent secret', async () => { const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); const schema = { type: 'object', properties: { api_key: { type: 'string', source: 'user_provided', }, }, }; writeFileSync(join(schemaDir, 'secrets.json'), JSON.stringify(schema, null, 2)); const metadata = await getSecretMetadata('test-module', 'non_existent', testDb); expect(metadata).toBeNull(); }); test('defaults to user_provided when source not specified', async () => { const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); const schema = { type: 'object', properties: { api_key: { type: 'string', description: 'API key', }, }, }; writeFileSync(join(schemaDir, 'secrets.json'), JSON.stringify(schema, null, 2)); const metadata = await getSecretMetadata('test-module', 'api_key', testDb); expect(metadata?.source).toBe('user_provided'); }); }); describe('getAllSecretMetadata', () => { test('returns all secret metadata', async () => { const schemaDir = join(tempDir, 'schema'); mkdirSync(schemaDir, { recursive: true }); const schema = { type: 'object', properties: { api_key: { type: 'string', source: 'user_provided', }, tsig_secret: { type: 'string', source: 'generated', format: 'tsig-key', }, admin_password: { type: 'string', source: 'generated_optional', format: 'base64', }, }, }; writeFileSync(join(schemaDir, 'secrets.json'), JSON.stringify(schema, null, 2)); const allMetadata = await getAllSecretMetadata('test-module', testDb); expect(allMetadata).toHaveLength(3); expect(allMetadata.map((m) => m.name)).toContain('api_key'); expect(allMetadata.map((m) => m.name)).toContain('tsig_secret'); expect(allMetadata.map((m) => m.name)).toContain('admin_password'); }); test('returns empty array when no schema exists', async () => { const allMetadata = await getAllSecretMetadata('test-module', testDb); expect(allMetadata).toEqual([]); }); }); describe('validateDerivationGraph', () => { test('validates schema without derivation', () => { const schema: SecretsSchema = { type: 'object', properties: { api_key: { type: 'string', source: 'user_provided', }, }, }; const error = validateDerivationGraph(schema); expect(error).toBeNull(); }); test('validates schema with linear derivation', () => { const schema: SecretsSchema = { type: 'object', properties: { private_key: { type: 'string', source: 'generated', format: 'wireguard-key', }, public_key: { type: 'string', source: 'generated', format: 'wireguard-key', derive_from: 'private_key', derive_method: 'wireguard-pubkey', }, }, }; const error = validateDerivationGraph(schema); expect(error).toBeNull(); }); test('detects circular derivation (direct cycle)', () => { const schema: SecretsSchema = { type: 'object', properties: { secret_a: { type: 'string', derive_from: 'secret_b', derive_method: 'test', }, secret_b: { type: 'string', derive_from: 'secret_a', derive_method: 'test', }, }, }; const error = validateDerivationGraph(schema); expect(error).not.toBeNull(); expect(error).toContain('Circular derivation detected'); }); test('detects circular derivation (indirect cycle)', () => { const schema: SecretsSchema = { type: 'object', properties: { secret_a: { type: 'string', derive_from: 'secret_b', derive_method: 'test', }, secret_b: { type: 'string', derive_from: 'secret_c', derive_method: 'test', }, secret_c: { type: 'string', derive_from: 'secret_a', derive_method: 'test', }, }, }; const error = validateDerivationGraph(schema); expect(error).not.toBeNull(); expect(error).toContain('Circular derivation detected'); }); }); });