import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { mkdtempSync, rmSync, statSync, existsSync, writeFileSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { FileSecretStore } from '../../src/secrets/file-store.js'; let tempDir: string; beforeEach(() => { tempDir = mkdtempSync(join(tmpdir(), 'mcpctl-secrets-test-')); }); afterEach(() => { rmSync(tempDir, { recursive: true, force: true }); }); describe('FileSecretStore', () => { it('returns null for missing key', async () => { const store = new FileSecretStore({ configDir: tempDir }); expect(await store.get('nonexistent')).toBeNull(); }); it('stores and retrieves a secret', async () => { const store = new FileSecretStore({ configDir: tempDir }); await store.set('api-key', 'sk-12345'); expect(await store.get('api-key')).toBe('sk-12345'); }); it('overwrites existing values', async () => { const store = new FileSecretStore({ configDir: tempDir }); await store.set('api-key', 'old-value'); await store.set('api-key', 'new-value'); expect(await store.get('api-key')).toBe('new-value'); }); it('stores multiple keys', async () => { const store = new FileSecretStore({ configDir: tempDir }); await store.set('key-a', 'value-a'); await store.set('key-b', 'value-b'); expect(await store.get('key-a')).toBe('value-a'); expect(await store.get('key-b')).toBe('value-b'); }); it('deletes a key', async () => { const store = new FileSecretStore({ configDir: tempDir }); await store.set('api-key', 'sk-12345'); expect(await store.delete('api-key')).toBe(true); expect(await store.get('api-key')).toBeNull(); }); it('returns false when deleting nonexistent key', async () => { const store = new FileSecretStore({ configDir: tempDir }); expect(await store.delete('nonexistent')).toBe(false); }); it('sets 0600 permissions on secrets file', async () => { const store = new FileSecretStore({ configDir: tempDir }); await store.set('api-key', 'sk-12345'); const stat = statSync(join(tempDir, 'secrets')); expect(stat.mode & 0o777).toBe(0o600); }); it('creates config dir if missing', async () => { const nested = join(tempDir, 'sub', 'dir'); const store = new FileSecretStore({ configDir: nested }); await store.set('api-key', 'sk-12345'); expect(existsSync(join(nested, 'secrets'))).toBe(true); }); it('recovers from corrupted JSON', async () => { writeFileSync(join(tempDir, 'secrets'), 'NOT JSON!!!', 'utf-8'); const store = new FileSecretStore({ configDir: tempDir }); // Should not throw, returns null for any key expect(await store.get('api-key')).toBeNull(); // Should be able to write over corrupted file await store.set('api-key', 'fresh-value'); expect(await store.get('api-key')).toBe('fresh-value'); }); it('reports file backend', () => { const store = new FileSecretStore({ configDir: tempDir }); expect(store.backend()).toBe('file'); }); it('preserves other keys on delete', async () => { const store = new FileSecretStore({ configDir: tempDir }); await store.set('key-a', 'value-a'); await store.set('key-b', 'value-b'); await store.delete('key-a'); expect(await store.get('key-a')).toBeNull(); expect(await store.get('key-b')).toBe('value-b'); }); });