import { describe, it, expect, vi } from 'vitest'; import { PlaintextDriver } from '../src/services/secret-backends/plaintext.js'; import { OpenBaoDriver } from '../src/services/secret-backends/openbao.js'; describe('PlaintextDriver', () => { const driver = new PlaintextDriver({ listAllPlaintext: async () => [{ name: 'a', data: { k: 'v' } }] }); it('read returns the data passed in', async () => { const result = await driver.read({ name: 's', externalRef: '', data: { token: 'abc' } }); expect(result).toEqual({ token: 'abc' }); }); it('write returns storedData = input, externalRef = empty', async () => { const result = await driver.write({ name: 's', data: { k: 'v' } }); expect(result).toEqual({ externalRef: '', storedData: { k: 'v' } }); }); it('list delegates to the injected dep', async () => { const list = await driver.list(); expect(list).toEqual([{ name: 'a', externalRef: '' }]); }); it('delete is a no-op', async () => { await expect(driver.delete({ name: 's', externalRef: '' })).resolves.toBeUndefined(); }); }); describe('OpenBaoDriver', () => { function makeFetch(responses: Array<{ url: RegExp; status: number; body?: unknown }>): ReturnType { return vi.fn(async (url: string | URL, _init?: RequestInit) => { const urlStr = String(url); const match = responses.find((r) => r.url.test(urlStr)); if (!match) throw new Error(`unexpected fetch: ${urlStr}`); return new Response(match.body ? JSON.stringify(match.body) : '', { status: match.status }); }); } const resolver = { resolve: vi.fn(async () => 'test-vault-token') }; it('write sends POST to .../data/ with {data: ...}', async () => { const fetchFn = makeFetch([{ url: /\/v1\/secret\/data\/mcpctl\/mytoken$/, status: 200 }]); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: resolver }, ); const result = await driver.write({ name: 'mytoken', data: { api_key: 'secret-xyz' } }); expect(result.externalRef).toBe('secret/mcpctl/mytoken'); expect(result.storedData).toEqual({}); expect(fetchFn).toHaveBeenCalledTimes(1); const [, init] = fetchFn.mock.calls[0] as [unknown, RequestInit]; expect(init.method).toBe('POST'); expect(JSON.parse(init.body as string)).toEqual({ data: { api_key: 'secret-xyz' } }); const headers = init.headers as Record; expect(headers['X-Vault-Token']).toBe('test-vault-token'); }); it('read returns body.data.data', async () => { const fetchFn = makeFetch([{ url: /\/v1\/secret\/data\/mcpctl\/mytoken$/, status: 200, body: { data: { data: { api_key: 'secret-xyz' } } }, }]); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: resolver }, ); const result = await driver.read({ name: 'mytoken', externalRef: 'secret/mcpctl/mytoken', data: {} }); expect(result).toEqual({ api_key: 'secret-xyz' }); }); it('read throws when the path 404s', async () => { const fetchFn = makeFetch([{ url: /\/data\//, status: 404 }]); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: resolver }, ); await expect(driver.read({ name: 'missing', externalRef: '', data: {} })).rejects.toThrow(/not found/); }); it('delete swallows 404', async () => { const fetchFn = makeFetch([{ url: /\/metadata\//, status: 404 }]); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: resolver }, ); await expect(driver.delete({ name: 'gone', externalRef: '' })).resolves.toBeUndefined(); }); it('list returns names from the metadata LIST call', async () => { const fetchFn = makeFetch([{ url: /\/v1\/secret\/metadata\/mcpctl\/$/, status: 200, body: { data: { keys: ['token1', 'token2', 'sub-folder/'] } }, }]); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: resolver }, ); const result = await driver.list(); // Sub-folders (trailing slash) are excluded; only leaf keys are returned. expect(result).toEqual([ { name: 'token1', externalRef: 'secret/mcpctl/token1' }, { name: 'token2', externalRef: 'secret/mcpctl/token2' }, ]); }); it('caches the vault token after first resolve', async () => { const fetchFn = makeFetch([ { url: /\/v1\/secret\/data\/mcpctl\//, status: 200, body: { data: { data: { k: 'v' } } } }, ]); const singleResolver = { resolve: vi.fn(async () => 'test-vault-token') }; const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: singleResolver }, ); await driver.read({ name: 'a', externalRef: '', data: {} }); await driver.read({ name: 'a', externalRef: '', data: {} }); expect(singleResolver.resolve).toHaveBeenCalledTimes(1); }); it('propagates X-Vault-Namespace when configured', async () => { const fetchFn = makeFetch([{ url: /\/v1\/secret\/data\/mcpctl\//, status: 200 }]); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', namespace: 'myteam', tokenSecretRef: { name: 'bao', key: 'token' } }, { fetch: fetchFn as unknown as typeof fetch, secretRefResolver: resolver }, ); await driver.write({ name: 'x', data: { k: 'v' } }); const [, init] = fetchFn.mock.calls[0] as [unknown, RequestInit]; const headers = init.headers as Record; expect(headers['X-Vault-Namespace']).toBe('myteam'); }); describe('kubernetes auth', () => { it('exchanges the SA JWT for a vault client token via /v1/auth/kubernetes/login', async () => { const calls: Array<{ url: string; init: RequestInit }> = []; const fetchFn = vi.fn(async (url: string | URL, init?: RequestInit) => { const u = String(url); calls.push({ url: u, init: init ?? {} }); if (u.endsWith('/v1/auth/kubernetes/login')) { return new Response(JSON.stringify({ auth: { client_token: 'vault.client.token.xyz', lease_duration: 3600 }, }), { status: 200 }); } return new Response(JSON.stringify({}), { status: 200 }); }); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', auth: 'kubernetes', role: 'mcpctl' }, { fetch: fetchFn as unknown as typeof fetch, readServiceAccountToken: async () => 'eyJ.fake.sa.jwt', }, ); await driver.write({ name: 'x', data: { k: 'v' } }); // Two calls: login + write expect(calls).toHaveLength(2); expect(calls[0]!.url).toBe('http://bao.example:8200/v1/auth/kubernetes/login'); expect(JSON.parse(calls[0]!.init.body as string)).toEqual({ role: 'mcpctl', jwt: 'eyJ.fake.sa.jwt' }); // Write uses the returned client token const writeHeaders = calls[1]!.init.headers as Record; expect(writeHeaders['X-Vault-Token']).toBe('vault.client.token.xyz'); }); it('caches the vault token across requests and renews after lease expiry', async () => { let nowMs = 1_000_000_000_000; let loginCount = 0; const fetchFn = vi.fn(async (url: string | URL) => { const u = String(url); if (u.endsWith('/v1/auth/kubernetes/login')) { loginCount++; // 600s lease leaves 540s of cached window after the 60s grace. return new Response(JSON.stringify({ auth: { client_token: `tok-${String(loginCount)}`, lease_duration: 600 }, }), { status: 200 }); } return new Response(JSON.stringify({}), { status: 200 }); }); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', auth: 'kubernetes', role: 'mcpctl' }, { fetch: fetchFn as unknown as typeof fetch, readServiceAccountToken: async () => 'jwt', now: () => nowMs, }, ); await driver.write({ name: 'a', data: { k: 'v' } }); await driver.write({ name: 'b', data: { k: 'v' } }); expect(loginCount).toBe(1); // both writes share the cached token // Advance past lease - grace window → driver re-logs in nowMs += 600_000; await driver.write({ name: 'c', data: { k: 'v' } }); expect(loginCount).toBe(2); }); it('honours custom authMount path', async () => { const calls: string[] = []; const fetchFn = vi.fn(async (url: string | URL) => { calls.push(String(url)); if (String(url).includes('/login')) { return new Response(JSON.stringify({ auth: { client_token: 't', lease_duration: 3600 } }), { status: 200 }); } return new Response(JSON.stringify({}), { status: 200 }); }); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', auth: 'kubernetes', role: 'mcpctl', authMount: 'kubernetes/cluster-a' }, { fetch: fetchFn as unknown as typeof fetch, readServiceAccountToken: async () => 'jwt' }, ); await driver.write({ name: 'x', data: {} }); expect(calls[0]).toBe('http://bao.example:8200/v1/auth/kubernetes/cluster-a/login'); }); it('throws on login failure with a clear error', async () => { const fetchFn = vi.fn(async () => new Response('permission denied', { status: 403 })); const driver = new OpenBaoDriver( { url: 'http://bao.example:8200', auth: 'kubernetes', role: 'mcpctl' }, { fetch: fetchFn as unknown as typeof fetch, readServiceAccountToken: async () => 'jwt' }, ); await expect(driver.read({ name: 'x', externalRef: '', data: {} })) .rejects.toThrow(/kubernetes login.*role=mcpctl.*HTTP 403/); }); it('rejects construction when role is missing', () => { expect(() => new OpenBaoDriver( // eslint-disable-next-line @typescript-eslint/no-explicit-any { url: 'http://bao.example:8200', auth: 'kubernetes' } as any, { fetch: vi.fn() as unknown as typeof fetch, readServiceAccountToken: async () => 'jwt' }, )).toThrow(/role.*required/); }); it('rejects token-auth construction when tokenSecretRef is missing', () => { expect(() => new OpenBaoDriver( // eslint-disable-next-line @typescript-eslint/no-explicit-any { url: 'http://bao.example:8200' } as any, { fetch: vi.fn() as unknown as typeof fetch, secretRefResolver: resolver }, )).toThrow(/tokenSecretRef.*required/); }); }); });