import { describe, it, expect, vi } from 'vitest'; import { GnomeKeyringStore } from '../../src/secrets/gnome-keyring.js'; import type { RunCommand } from '../../src/secrets/gnome-keyring.js'; function mockRun( responses: Record, ): RunCommand { return vi.fn(async (cmd: string, args: string[], _stdin?: string) => { const key = `${cmd} ${args.join(' ')}`; for (const [pattern, response] of Object.entries(responses)) { if (key.includes(pattern)) return response; } return { stdout: '', code: 1 }; }); } describe('GnomeKeyringStore', () => { describe('get', () => { it('returns value on success', async () => { const run = mockRun({ 'lookup': { stdout: 'my-secret', code: 0 } }); const store = new GnomeKeyringStore({ run }); expect(await store.get('api-key')).toBe('my-secret'); }); it('returns null on exit code 1', async () => { const run = mockRun({ 'lookup': { stdout: '', code: 1 } }); const store = new GnomeKeyringStore({ run }); expect(await store.get('api-key')).toBeNull(); }); it('returns null on empty stdout', async () => { const run = mockRun({ 'lookup': { stdout: '', code: 0 } }); const store = new GnomeKeyringStore({ run }); expect(await store.get('api-key')).toBeNull(); }); it('returns null on error', async () => { const run = vi.fn().mockRejectedValue(new Error('timeout')); const store = new GnomeKeyringStore({ run }); expect(await store.get('api-key')).toBeNull(); }); it('calls secret-tool with correct args', async () => { const run = vi.fn().mockResolvedValue({ stdout: 'val', code: 0 }); const store = new GnomeKeyringStore({ run }); await store.get('my-key'); expect(run).toHaveBeenCalledWith( 'secret-tool', ['lookup', 'service', 'mcpctl', 'key', 'my-key'], ); }); }); describe('set', () => { it('calls secret-tool store with value as stdin', async () => { const run = vi.fn().mockResolvedValue({ stdout: '', code: 0 }); const store = new GnomeKeyringStore({ run }); await store.set('api-key', 'secret-value'); expect(run).toHaveBeenCalledWith( 'secret-tool', ['store', '--label', 'mcpctl: api-key', 'service', 'mcpctl', 'key', 'api-key'], 'secret-value', ); }); it('throws on non-zero exit code', async () => { const run = vi.fn().mockResolvedValue({ stdout: '', code: 1 }); const store = new GnomeKeyringStore({ run }); await expect(store.set('api-key', 'val')).rejects.toThrow('exited with code 1'); }); }); describe('delete', () => { it('returns true on success', async () => { const run = mockRun({ 'clear': { stdout: '', code: 0 } }); const store = new GnomeKeyringStore({ run }); expect(await store.delete('api-key')).toBe(true); }); it('returns false on failure', async () => { const run = mockRun({ 'clear': { stdout: '', code: 1 } }); const store = new GnomeKeyringStore({ run }); expect(await store.delete('api-key')).toBe(false); }); it('returns false on error', async () => { const run = vi.fn().mockRejectedValue(new Error('fail')); const store = new GnomeKeyringStore({ run }); expect(await store.delete('api-key')).toBe(false); }); it('calls secret-tool clear with correct args', async () => { const run = vi.fn().mockResolvedValue({ stdout: '', code: 0 }); const store = new GnomeKeyringStore({ run }); await store.delete('my-key'); expect(run).toHaveBeenCalledWith( 'secret-tool', ['clear', 'service', 'mcpctl', 'key', 'my-key'], ); }); }); describe('isAvailable', () => { it('returns true when secret-tool exists', async () => { const run = vi.fn().mockResolvedValue({ stdout: '0.20', code: 0 }); expect(await GnomeKeyringStore.isAvailable({ run })).toBe(true); }); it('returns false when secret-tool not found', async () => { const run = vi.fn().mockRejectedValue(new Error('ENOENT')); expect(await GnomeKeyringStore.isAvailable({ run })).toBe(false); }); it('returns false on non-zero exit', async () => { const run = vi.fn().mockResolvedValue({ stdout: '', code: 127 }); expect(await GnomeKeyringStore.isAvailable({ run })).toBe(false); }); }); it('reports gnome-keyring backend', () => { const run = vi.fn().mockResolvedValue({ stdout: '', code: 0 }); const store = new GnomeKeyringStore({ run }); expect(store.backend()).toBe('gnome-keyring'); }); });