133 lines
6.0 KiB
TypeScript
133 lines
6.0 KiB
TypeScript
|
|
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<typeof vi.fn> {
|
||
|
|
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/<path> 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<string, string>;
|
||
|
|
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<string, string>;
|
||
|
|
expect(headers['X-Vault-Namespace']).toBe('myteam');
|
||
|
|
});
|
||
|
|
});
|