/** * Unit tests for `{env:VAR}` substitution in the MCP config loader. * Pure — no live MCP server. Verifies secrets resolve from process.env * (so real keys live in `.env`, not the gitignored config file). */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { substituteEnvVars } from '../mcp-config.js'; // Minimal FastifyBaseLogger stub — only .warn is exercised here. function fakeLog() { const warnings: string[] = []; const log = { warn: (msg: unknown) => { warnings.push(typeof msg === 'string' ? msg : JSON.stringify(msg)); }, }; return { log: log as never, warnings }; } describe('substituteEnvVars', () => { const SAVED = process.env.MCP_TEST_SECRET; beforeEach(() => { process.env.MCP_TEST_SECRET = 'resolved-value'; }); afterEach(() => { if (SAVED === undefined) delete process.env.MCP_TEST_SECRET; else process.env.MCP_TEST_SECRET = SAVED; delete process.env.MCP_TEST_MISSING; }); it('replaces a {env:VAR} reference in a string value', () => { const { log } = fakeLog(); expect(substituteEnvVars('{env:MCP_TEST_SECRET}', log)).toBe('resolved-value'); }); it('substitutes inside nested objects and arrays', () => { const { log } = fakeLog(); const out = substituteEnvVars( { headers: { CONTEXT7_API_KEY: '{env:MCP_TEST_SECRET}' }, args: ['--token', '{env:MCP_TEST_SECRET}'], }, log, ); expect(out).toEqual({ headers: { CONTEXT7_API_KEY: 'resolved-value' }, args: ['--token', 'resolved-value'], }); }); it('leaves object keys untouched, only transforms values', () => { const { log } = fakeLog(); const out = substituteEnvVars({ '{env:MCP_TEST_SECRET}': 'literal' }, log) as Record; expect(Object.keys(out)).toEqual(['{env:MCP_TEST_SECRET}']); }); it('resolves an unset var to empty string and warns', () => { const { log, warnings } = fakeLog(); expect(substituteEnvVars('{env:MCP_TEST_MISSING}', log)).toBe(''); expect(warnings.some((w) => w.includes('MCP_TEST_MISSING'))).toBe(true); }); it('passes non-string scalars through unchanged', () => { const { log } = fakeLog(); expect(substituteEnvVars(true, log)).toBe(true); expect(substituteEnvVars(42, log)).toBe(42); expect(substituteEnvVars(null, log)).toBe(null); }); it('leaves strings without a reference unchanged', () => { const { log } = fakeLog(); expect(substituteEnvVars('https://mcp.context7.com/mcp', log)).toBe('https://mcp.context7.com/mcp'); }); it('resolves multiple references in one string (global flag)', () => { const { log } = fakeLog(); expect(substituteEnvVars('{env:MCP_TEST_SECRET}/{env:MCP_TEST_SECRET}', log)).toBe( 'resolved-value/resolved-value', ); }); it('passes an empty string through unchanged', () => { const { log } = fakeLog(); expect(substituteEnvVars('', log)).toBe(''); }); it('collects unset var names into the optional collector set', () => { const { log } = fakeLog(); const unset = new Set(); substituteEnvVars({ url: '{env:MCP_TEST_MISSING}', headers: { k: '{env:MCP_TEST_SECRET}' } }, log, unset); expect([...unset]).toEqual(['MCP_TEST_MISSING']); }); });