envProcessor.test.ts•8.18 kB
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
import { parseEnvArray, processEnvironment, substituteEnvVars, substituteEnvVarsInConfig } from './envProcessor.js';
// Mock getDefaultEnvironment to avoid SDK dependency in tests
vi.mock('@modelcontextprotocol/sdk/client/stdio.js', () => ({
  getDefaultEnvironment: vi.fn().mockReturnValue({
    HOME: '/home/user',
    PATH: '/usr/bin',
  }),
}));
describe('EnvProcessor', () => {
  const originalEnv = process.env;
  beforeEach(() => {
    // Reset process.env for each test
    process.env = { ...originalEnv };
    vi.clearAllMocks();
  });
  afterEach(() => {
    process.env = originalEnv;
  });
  describe('parseEnvArray', () => {
    it('should parse key=value format', () => {
      const result = parseEnvArray(['NODE_ENV=production', 'DEBUG=true']);
      expect(result).toEqual({
        NODE_ENV: 'production',
        DEBUG: 'true',
      });
    });
    it('should handle inheritance for keys without equals', () => {
      process.env.EXISTING_VAR = 'test-value';
      process.env.ANOTHER_VAR = 'another-value';
      const result = parseEnvArray(['EXISTING_VAR', 'ANOTHER_VAR', 'NON_EXISTENT']);
      expect(result).toEqual({
        EXISTING_VAR: 'test-value',
        ANOTHER_VAR: 'another-value',
        // NON_EXISTENT should not be present since it doesn't exist
      });
    });
    it('should handle mixed format', () => {
      process.env.PATH = '/usr/bin';
      const result = parseEnvArray(['PATH', 'NODE_ENV=production']);
      expect(result).toEqual({
        PATH: '/usr/bin',
        NODE_ENV: 'production',
      });
    });
    it('should handle empty values', () => {
      const result = parseEnvArray(['EMPTY=', 'NORMAL=value']);
      expect(result).toEqual({
        EMPTY: '',
        NORMAL: 'value',
      });
    });
  });
  describe('substituteEnvVars', () => {
    it('should substitute single environment variable', () => {
      process.env.API_KEY = 'secret-key';
      const result = substituteEnvVars('${API_KEY}');
      expect(result).toBe('secret-key');
    });
    it('should substitute multiple environment variables', () => {
      process.env.HOST = 'localhost';
      process.env.PORT = '3000';
      const result = substituteEnvVars('http://${HOST}:${PORT}/api');
      expect(result).toBe('http://localhost:3000/api');
    });
    it('should handle missing environment variables', () => {
      const result = substituteEnvVars('${MISSING_VAR}');
      expect(result).toBe('${MISSING_VAR}'); // Should keep placeholder
    });
    it('should handle whitespace in variable names', () => {
      process.env.SPACED_VAR = 'value';
      const result = substituteEnvVars('${ SPACED_VAR }');
      expect(result).toBe('value');
    });
    it('should not substitute malformed patterns', () => {
      const result = substituteEnvVars('${INCOMPLETE');
      expect(result).toBe('${INCOMPLETE');
    });
  });
  describe('substituteEnvVarsInConfig', () => {
    it('should substitute in string values', () => {
      process.env.DB_HOST = 'localhost';
      const config = {
        database: {
          host: '${DB_HOST}',
          port: 5432,
        },
      };
      const result = substituteEnvVarsInConfig(config);
      expect(result).toEqual({
        database: {
          host: 'localhost',
          port: 5432,
        },
      });
    });
    it('should handle arrays with substitution', () => {
      process.env.CMD = 'node';
      process.env.ARG = 'server.js';
      const config = {
        command: '${CMD}',
        args: ['${ARG}', '--port', '3000'],
      };
      const result = substituteEnvVarsInConfig(config);
      expect(result).toEqual({
        command: 'node',
        args: ['server.js', '--port', '3000'],
      });
    });
    it('should preserve non-string values', () => {
      const config = {
        enabled: true,
        timeout: 5000,
        data: null,
        nested: {
          flag: false,
        },
      };
      const result = substituteEnvVarsInConfig(config);
      expect(result).toEqual(config);
    });
  });
  describe('processEnvironment', () => {
    beforeEach(() => {
      // Set up a clean test environment
      process.env = {
        HOME: '/home/user',
        PATH: '/usr/bin',
        NODE_ENV: 'test',
        DEBUG: 'true',
        SENSITIVE_DATA: 'secret',
        CUSTOM_VAR: 'custom-value',
      };
    });
    it('should process environment with SDK defaults only', () => {
      const result = processEnvironment({});
      expect(result.processedEnv).toHaveProperty('HOME');
      expect(result.processedEnv).toHaveProperty('PATH');
      expect(result.sources.sdkDefaults).toContain('HOME');
      expect(result.sources.sdkDefaults).toContain('PATH');
      expect(result.sources.inherited).toHaveLength(0);
      expect(result.sources.custom).toHaveLength(0);
    });
    it('should inherit parent environment when enabled', () => {
      const result = processEnvironment({
        inheritParentEnv: true,
      });
      expect(result.processedEnv).toHaveProperty('NODE_ENV', 'test');
      expect(result.processedEnv).toHaveProperty('DEBUG', 'true');
      expect(result.sources.inherited.length).toBeGreaterThan(0);
    });
    it('should apply environment filters', () => {
      const result = processEnvironment({
        inheritParentEnv: true,
        envFilter: ['NODE_*', 'DEBUG', '!SENSITIVE_*'],
      });
      expect(result.processedEnv).toHaveProperty('NODE_ENV', 'test');
      expect(result.processedEnv).toHaveProperty('DEBUG', 'true');
      expect(result.processedEnv).not.toHaveProperty('SENSITIVE_DATA');
      expect(result.sources.filtered).toContain('SENSITIVE_DATA');
    });
    it('should handle custom environment variables', () => {
      process.env.EXTERNAL_API = 'https://api.example.com';
      const result = processEnvironment({
        env: {
          API_KEY: 'test-key',
          ENDPOINT: '${EXTERNAL_API}/v1',
        },
      });
      expect(result.processedEnv).toHaveProperty('API_KEY', 'test-key');
      expect(result.processedEnv).toHaveProperty('ENDPOINT', 'https://api.example.com/v1');
      expect(result.sources.custom).toEqual(['API_KEY', 'ENDPOINT']);
    });
    it('should handle array format environment variables', () => {
      process.env.EXISTING_VAR = 'existing-value';
      const result = processEnvironment({
        env: ['EXISTING_VAR', 'NEW_VAR=new-value'],
      });
      expect(result.processedEnv).toHaveProperty('EXISTING_VAR', 'existing-value');
      expect(result.processedEnv).toHaveProperty('NEW_VAR', 'new-value');
      expect(result.sources.custom).toEqual(['EXISTING_VAR', 'NEW_VAR']);
    });
    it('should override inherited variables with custom ones', () => {
      const result = processEnvironment({
        inheritParentEnv: true,
        env: {
          NODE_ENV: 'overridden',
        },
      });
      expect(result.processedEnv).toHaveProperty('NODE_ENV', 'overridden');
    });
    it('should handle complex filtering scenarios', () => {
      process.env = {
        ...process.env,
        NODE_VERSION: '18.0.0',
        NODE_OPTIONS: '--max-old-space-size=4096',
        NPM_CONFIG_PREFIX: '/usr/local',
        BASH_FUNC_test: '() { echo test; }',
        SECRET_KEY: 'very-secret',
        PUBLIC_VAR: 'public-value',
      };
      const result = processEnvironment({
        inheritParentEnv: true,
        envFilter: [
          'NODE_*', // Allow NODE_* variables
          'NPM_*', // Allow NPM_* variables
          'PUBLIC_*', // Allow PUBLIC_* variables
          '!SECRET_*', // Block SECRET_* variables
          '!BASH_FUNC_*', // Block bash functions
        ],
      });
      expect(result.processedEnv).toHaveProperty('NODE_VERSION');
      expect(result.processedEnv).toHaveProperty('NODE_OPTIONS');
      expect(result.processedEnv).toHaveProperty('NPM_CONFIG_PREFIX');
      expect(result.processedEnv).toHaveProperty('PUBLIC_VAR');
      expect(result.processedEnv).not.toHaveProperty('SECRET_KEY');
      expect(result.processedEnv).not.toHaveProperty('BASH_FUNC_test');
    });
  });
});