/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import { resolveEnvVarsInString, resolveEnvVarsInObject, } from './envVarResolver.js'; describe('resolveEnvVarsInString', () => { beforeEach(() => { vi.stubEnv('TEST_VAR', ''); }); afterEach(() => { vi.unstubAllEnvs(); }); it('should resolve $VAR_NAME format', () => { vi.stubEnv('TEST_VAR', 'test-value'); const result = resolveEnvVarsInString('Value is $TEST_VAR'); expect(result).toBe('Value is test-value'); }); it('should resolve ${VAR_NAME} format', () => { vi.stubEnv('TEST_VAR', 'test-value'); const result = resolveEnvVarsInString('Value is ${TEST_VAR}'); expect(result).toBe('Value is test-value'); }); it('should resolve multiple variables', () => { vi.stubEnv('HOST', 'localhost'); vi.stubEnv('PORT', '8080'); const result = resolveEnvVarsInString('URL: http://$HOST:${PORT}/api'); expect(result).toBe('URL: http://localhost:8080/api'); }); it('should support environment variables with dots', () => { vi.stubEnv('FOO.BAR', 'baz'); const result = resolveEnvVarsInString('Value: ${FOO.BAR}'); expect(result).toBe('Value: baz'); }); it('should leave undefined variables unchanged', () => { const result = resolveEnvVarsInString('Value is $UNDEFINED_VAR'); expect(result).toBe('Value is $UNDEFINED_VAR'); }); it('should leave undefined variables with braces unchanged', () => { const result = resolveEnvVarsInString('Value is ${UNDEFINED_VAR}'); expect(result).toBe('Value is ${UNDEFINED_VAR}'); }); it('should handle empty string', () => { const result = resolveEnvVarsInString(''); expect(result).toBe(''); }); it('should handle string without variables', () => { const result = resolveEnvVarsInString('No variables here'); expect(result).toBe('No variables here'); }); it('should handle mixed defined and undefined variables', () => { vi.stubEnv('DEFINED', 'value'); const result = resolveEnvVarsInString('$DEFINED and $UNDEFINED mixed'); expect(result).toBe('value and $UNDEFINED mixed'); }); it('should use default value when environment variable is missing', () => { const result = resolveEnvVarsInString( 'URL: ${MISSING_VAR:-https://default.example.com}/api', ); expect(result).toBe('URL: https://default.example.com/api'); }); it('should ignore default value when environment variable is present', () => { vi.stubEnv('PRESENT_VAR', 'https://actual.example.com'); const result = resolveEnvVarsInString( 'URL: ${PRESENT_VAR:-https://default.example.com}/api', ); expect(result).toBe('URL: https://actual.example.com/api'); }); it('should support empty default value', () => { const result = resolveEnvVarsInString('Value: ${MISSING_VAR:-}'); expect(result).toBe('Value: '); }); it('should correctly handle default values that contain colons or dashes', () => { const result = resolveEnvVarsInString( 'Value: ${MISSING_VAR:-val:-123-abc}', ); expect(result).toBe('Value: val:-123-abc'); }); }); describe('resolveEnvVarsInObject', () => { afterEach(() => { vi.unstubAllEnvs(); }); it('should resolve variables in nested objects', () => { vi.stubEnv('API_KEY', 'secret-123'); vi.stubEnv('DB_URL', 'postgresql://localhost/test'); const config = { server: { auth: { key: '$API_KEY', }, database: '${DB_URL}', }, port: 3000, }; const result = resolveEnvVarsInObject(config); expect(result).toEqual({ server: { auth: { key: 'secret-123', }, database: 'postgresql://localhost/test', }, port: 3000, }); }); it('should resolve variables in arrays', () => { vi.stubEnv('ENV', 'production'); vi.stubEnv('VERSION', '1.0.0'); const config = { tags: ['$ENV', 'app', '${VERSION}'], metadata: { env: '$ENV', }, }; const result = resolveEnvVarsInObject(config); expect(result).toEqual({ tags: ['production', 'app', '1.0.0'], metadata: { env: 'production', }, }); }); it('should preserve non-string types', () => { const config = { enabled: true, count: 42, value: null, data: undefined, tags: ['item1', 'item2'], }; const result = resolveEnvVarsInObject(config); expect(result).toEqual(config); }); it('should handle MCP server config structure', () => { vi.stubEnv('API_TOKEN', 'token-123'); vi.stubEnv('SERVER_PORT', '8080'); const extensionConfig = { name: 'test-extension', version: '1.0.0', mcpServers: { 'test-server': { command: 'node', args: ['server.js', '--port', '${SERVER_PORT}'], env: { API_KEY: '$API_TOKEN', STATIC_VALUE: 'unchanged', }, timeout: 5000, }, }, }; const result = resolveEnvVarsInObject(extensionConfig); expect(result).toEqual({ name: 'test-extension', version: '1.0.0', mcpServers: { 'test-server': { command: 'node', args: ['server.js', '--port', '8080'], env: { API_KEY: 'token-123', STATIC_VALUE: 'unchanged', }, timeout: 5000, }, }, }); }); it('should handle empty and null values', () => { const config = { empty: '', nullValue: null, undefinedValue: undefined, zero: 0, false: false, }; const result = resolveEnvVarsInObject(config); expect(result).toEqual(config); }); it('should handle circular references in objects without infinite recursion', () => { vi.stubEnv('TEST_VAR', 'resolved-value'); type ConfigWithCircularRef = { name: string; value: number; self?: ConfigWithCircularRef; }; const config: ConfigWithCircularRef = { name: '$TEST_VAR', value: 42, }; // Create circular reference config.self = config; const result = resolveEnvVarsInObject(config); expect(result.name).toBe('resolved-value'); expect(result.value).toBe(42); expect(result.self).toBeDefined(); expect(result.self?.name).toBe('$TEST_VAR'); // Circular reference should be shallow copied expect(result.self?.value).toBe(42); // Verify it doesn't create infinite recursion by checking it's not the same object expect(result.self).not.toBe(result); }); it('should handle circular references in arrays without infinite recursion', () => { vi.stubEnv('ARRAY_VAR', 'array-value'); type ArrayWithCircularRef = Array; const arr: ArrayWithCircularRef = ['$ARRAY_VAR', 123]; // Create circular reference arr.push(arr); const result = resolveEnvVarsInObject(arr); expect(result[0]).toBe('array-value'); expect(result[1]).toBe(123); expect(Array.isArray(result[2])).toBe(true); const subArray = result[2] as ArrayWithCircularRef; expect(subArray[0]).toBe('$ARRAY_VAR'); // Circular reference should be shallow copied expect(subArray[1]).toBe(123); // Verify it doesn't create infinite recursion expect(result[2]).not.toBe(result); }); it('should handle complex nested circular references', () => { vi.stubEnv('NESTED_VAR', 'nested-resolved'); type ObjWithRef = { name: string; id: number; ref?: ObjWithRef; }; const obj1: ObjWithRef = { name: '$NESTED_VAR', id: 1 }; const obj2: ObjWithRef = { name: 'static', id: 2 }; // Create cross-references obj1.ref = obj2; obj2.ref = obj1; const config = { primary: obj1, secondary: obj2, value: '$NESTED_VAR', }; const result = resolveEnvVarsInObject(config); expect(result.value).toBe('nested-resolved'); expect(result.primary.name).toBe('nested-resolved'); expect(result.primary.id).toBe(1); expect(result.secondary.name).toBe('static'); expect(result.secondary.id).toBe(2); // Check that circular references are handled (shallow copied) expect(result.primary.ref).toBeDefined(); expect(result.secondary.ref).toBeDefined(); expect(result.primary.ref?.name).toBe('static'); // Should be shallow copy expect(result.secondary.ref?.name).toBe('nested-resolved'); // The shallow copy still gets processed // Most importantly: verify no infinite recursion by checking objects are different expect(result.primary.ref).not.toBe(result.secondary); expect(result.secondary.ref).not.toBe(result.primary); expect(result.primary).not.toBe(obj1); // New object created expect(result.secondary).not.toBe(obj2); // New object created }); });