import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { AutomationBridge } from '../../../src/automation/bridge.js'; describe('AutomationBridge Host Validation', () => { const originalEnv = process.env; beforeEach(() => { vi.resetModules(); process.env = { ...originalEnv }; delete process.env.MCP_AUTOMATION_ALLOW_NON_LOOPBACK; delete process.env.MCP_AUTOMATION_HOST; delete process.env.MCP_AUTOMATION_WS_HOST; delete process.env.MCP_AUTOMATION_WS_PORT; delete process.env.MCP_AUTOMATION_CLIENT_PORT; delete process.env.MCP_AUTOMATION_PORT; }); afterEach(() => { process.env = originalEnv; }); describe('Loopback addresses (always allowed)', () => { it('should accept 127.0.0.1 by default', () => { const bridge = new AutomationBridge({ host: '127.0.0.1', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should normalize localhost to 127.0.0.1', () => { const bridge = new AutomationBridge({ host: 'localhost', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should accept IPv6 loopback ::1', () => { const bridge = new AutomationBridge({ host: '::1', port: 8091 }); expect(bridge.getStatus().host).toBe('::1'); }); it('should accept bracketed IPv6 loopback [::1]', () => { const bridge = new AutomationBridge({ host: '[::1]', port: 8091 }); expect(bridge.getStatus().host).toBe('::1'); }); }); describe('Non-loopback addresses (default: rejected)', () => { it('should reject 0.0.0.0 by default and fallback to 127.0.0.1', () => { const bridge = new AutomationBridge({ host: '0.0.0.0', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should reject LAN IP by default and fallback to 127.0.0.1', () => { const bridge = new AutomationBridge({ host: '192.168.1.100', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should reject public IP by default and fallback to 127.0.0.1', () => { const bridge = new AutomationBridge({ host: '8.8.8.8', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); }); describe('Non-loopback addresses with allowNonLoopback=true (option)', () => { it('should accept 0.0.0.0 when allowNonLoopback is true', () => { const bridge = new AutomationBridge({ host: '0.0.0.0', port: 8091, allowNonLoopback: true }); expect(bridge.getStatus().host).toBe('0.0.0.0'); }); it('should accept LAN IP when allowNonLoopback is true', () => { const bridge = new AutomationBridge({ host: '192.168.1.100', port: 8091, allowNonLoopback: true }); expect(bridge.getStatus().host).toBe('192.168.1.100'); }); it('should accept any valid IPv4 when allowNonLoopback is true', () => { const bridge = new AutomationBridge({ host: '10.0.0.1', port: 8091, allowNonLoopback: true }); expect(bridge.getStatus().host).toBe('10.0.0.1'); }); it('should reject invalid hostname format even with allowNonLoopback', () => { const bridge = new AutomationBridge({ host: '-invalid-hostname', port: 8091, allowNonLoopback: true }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should reject out-of-range IPv4 octets', () => { const bridge = new AutomationBridge({ host: '256.1.1.1', port: 8091, allowNonLoopback: true }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); }); describe('Non-loopback addresses with MCP_AUTOMATION_ALLOW_NON_LOOPBACK env var', () => { it('should accept 0.0.0.0 when env var is "true"', () => { process.env.MCP_AUTOMATION_ALLOW_NON_LOOPBACK = 'true'; const bridge = new AutomationBridge({ host: '0.0.0.0', port: 8091 }); expect(bridge.getStatus().host).toBe('0.0.0.0'); }); it('should accept LAN IP when env var is "TRUE" (case insensitive)', () => { process.env.MCP_AUTOMATION_ALLOW_NON_LOOPBACK = 'TRUE'; const bridge = new AutomationBridge({ host: '192.168.1.50', port: 8091 }); expect(bridge.getStatus().host).toBe('192.168.1.50'); }); it('should reject non-loopback when env var is "false"', () => { process.env.MCP_AUTOMATION_ALLOW_NON_LOOPBACK = 'false'; const bridge = new AutomationBridge({ host: '0.0.0.0', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should reject non-loopback when env var is empty', () => { process.env.MCP_AUTOMATION_ALLOW_NON_LOOPBACK = ''; const bridge = new AutomationBridge({ host: '0.0.0.0', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); }); describe('Edge cases', () => { it('should handle empty host string', () => { const bridge = new AutomationBridge({ host: '', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should handle whitespace-only host', () => { const bridge = new AutomationBridge({ host: ' ', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should trim whitespace from valid host', () => { const bridge = new AutomationBridge({ host: ' 127.0.0.1 ', port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('should use default when no host provided', () => { const bridge = new AutomationBridge({ port: 8091 }); expect(bridge.getStatus().host).toBe('127.0.0.1'); }); it('option allowNonLoopback should override env var false', () => { process.env.MCP_AUTOMATION_ALLOW_NON_LOOPBACK = 'false'; const bridge = new AutomationBridge({ host: '0.0.0.0', port: 8091, allowNonLoopback: true }); expect(bridge.getStatus().host).toBe('0.0.0.0'); }); }); });