""" Tests for MCP API endpoints with HTTP and Docker socket modes. """ import os import sys from unittest.mock import AsyncMock, MagicMock, patch import httpx import pytest from src.server.api_routes.mcp_api import ( get_container_status, get_container_status_docker, get_container_status_http, ) from src.server.config.config import MCPMonitoringConfig @pytest.fixture def mock_mcp_url(): """Mock MCP URL for testing.""" return "http://test-mcp:8051" @pytest.fixture def mock_config_http(): """Mock configuration with HTTP mode enabled.""" return MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) @pytest.fixture def mock_config_docker(): """Mock configuration with Docker socket mode enabled.""" return MCPMonitoringConfig(enable_docker_socket=True, health_check_timeout=5) # HTTP Mode Tests @pytest.mark.asyncio async def test_get_container_status_http_running(mock_mcp_url): """Test HTTP health check when MCP server is running.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True, "uptime_seconds": 123.45, "health": {}} mock_response.status_code = 200 with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) # Create mock async context manager mock_client = MagicMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status_http() assert result["status"] == "running" assert result["uptime"] == 123 assert result["logs"] == [] mock_client.get.assert_called_once_with(f"{mock_mcp_url}/health") @pytest.mark.asyncio async def test_get_container_status_http_unreachable(mock_mcp_url): """Test HTTP health check when MCP server is unreachable.""" with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) # Mock connection error mock_client = MagicMock() mock_client.get = AsyncMock(side_effect=httpx.ConnectError("Connection refused")) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status_http() assert result["status"] == "unreachable" assert result["uptime"] is None assert result["logs"] == [] @pytest.mark.asyncio async def test_get_container_status_http_timeout(mock_mcp_url): """Test HTTP health check when MCP server times out.""" with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) # Mock timeout error mock_client = MagicMock() mock_client.get = AsyncMock(side_effect=httpx.TimeoutException("Timeout")) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status_http() assert result["status"] == "unhealthy" assert result["uptime"] is None assert result["logs"] == [] @pytest.mark.asyncio async def test_get_container_status_http_unhealthy(mock_mcp_url): """Test HTTP health check when MCP server reports unhealthy.""" mock_response = MagicMock() mock_response.json.return_value = {"success": False, "error": "Service unavailable"} mock_response.status_code = 200 with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) mock_client = MagicMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status_http() assert result["status"] == "unhealthy" assert result["uptime"] is None assert result["logs"] == [] @pytest.mark.asyncio async def test_get_container_status_http_zero_uptime(mock_mcp_url): """Test HTTP health check preserves 0 uptime for freshly-launched MCP.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True, "uptime_seconds": 0, "health": {}} mock_response.status_code = 200 with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) mock_client = MagicMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status_http() assert result["status"] == "running" assert result["uptime"] == 0 # Important: 0 should be preserved, not None assert result["logs"] == [] mock_client.get.assert_called_once_with(f"{mock_mcp_url}/health") @pytest.mark.asyncio async def test_get_container_status_http_error(mock_mcp_url): """Test HTTP health check when an unexpected error occurs.""" with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) # Mock unexpected error mock_client = MagicMock() mock_client.get = AsyncMock(side_effect=Exception("Unexpected error")) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status_http() assert result["status"] == "error" assert result["uptime"] is None assert result["logs"] == [] # Docker Mode Tests def test_get_container_status_docker_running(): """Test Docker socket check when container is running.""" mock_container = MagicMock() mock_container.status = "running" mock_container.attrs = { "State": {"StartedAt": "2025-01-01T00:00:00.000000000Z"}, } mock_docker_client = MagicMock() mock_docker_client.containers.get.return_value = mock_container # Create mock docker module with errors submodule mock_docker = MagicMock() mock_docker.from_env.return_value = mock_docker_client mock_docker_errors = MagicMock() mock_docker_errors.NotFound = type("NotFound", (Exception,), {}) with patch.dict("sys.modules", {"docker": mock_docker, "docker.errors": mock_docker_errors}): result = get_container_status_docker() assert result["status"] == "running" assert result["uptime"] is not None # Uptime should be calculated assert result["logs"] == [] mock_docker_client.containers.get.assert_called_once_with("archon-mcp") mock_docker_client.close.assert_called_once() def test_get_container_status_docker_stopped(): """Test Docker socket check when container is stopped.""" mock_container = MagicMock() mock_container.status = "exited" mock_docker_client = MagicMock() mock_docker_client.containers.get.return_value = mock_container # Create mock docker module with errors submodule mock_docker = MagicMock() mock_docker.from_env.return_value = mock_docker_client mock_docker_errors = MagicMock() mock_docker_errors.NotFound = type("NotFound", (Exception,), {}) with patch.dict("sys.modules", {"docker": mock_docker, "docker.errors": mock_docker_errors}): result = get_container_status_docker() assert result["status"] == "stopped" assert result["uptime"] is None assert result["logs"] == [] mock_docker_client.close.assert_called_once() def test_get_container_status_docker_not_found(): """Test Docker socket check when container is not found.""" # Create a mock NotFound exception mock_not_found = type("NotFound", (Exception,), {}) mock_docker_client = MagicMock() mock_docker_client.containers.get.side_effect = mock_not_found("Container not found") mock_docker = MagicMock() mock_docker.from_env.return_value = mock_docker_client mock_docker.errors = MagicMock() mock_docker.errors.NotFound = mock_not_found with patch.dict("sys.modules", {"docker": mock_docker, "docker.errors": mock_docker.errors}): result = get_container_status_docker() assert result["status"] == "not_found" assert result["uptime"] is None assert result["logs"] == [] assert "message" in result mock_docker_client.close.assert_called_once() def test_get_container_status_docker_error(): """Test Docker socket check when an error occurs.""" mock_docker_client = MagicMock() mock_docker_client.containers.get.side_effect = Exception("Docker error") # Create mock docker module with errors submodule mock_docker = MagicMock() mock_docker.from_env.return_value = mock_docker_client mock_docker_errors = MagicMock() mock_docker_errors.NotFound = type("NotFound", (Exception,), {}) with patch.dict("sys.modules", {"docker": mock_docker, "docker.errors": mock_docker_errors}): result = get_container_status_docker() assert result["status"] == "error" assert result["uptime"] is None assert result["logs"] == [] assert "error" in result mock_docker_client.close.assert_called_once() # Routing Tests @pytest.mark.asyncio async def test_get_container_status_routes_to_http(mock_mcp_url): """Test that get_container_status routes to HTTP mode by default.""" mock_response = MagicMock() mock_response.json.return_value = {"success": True, "uptime_seconds": 100, "health": {}} mock_response.status_code = 200 with ( patch("src.server.api_routes.mcp_api.get_mcp_url", return_value=mock_mcp_url), patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch("httpx.AsyncClient") as mock_client_class, ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=False, health_check_timeout=5) mock_client = MagicMock() mock_client.get = AsyncMock(return_value=mock_response) mock_client_class.return_value.__aenter__.return_value = mock_client mock_client_class.return_value.__aexit__.return_value = None result = await get_container_status() assert result["status"] == "running" assert result["uptime"] == 100 mock_client.get.assert_called_once() @pytest.mark.asyncio async def test_get_container_status_routes_to_docker(): """Test that get_container_status routes to Docker mode when enabled.""" mock_container = MagicMock() mock_container.status = "running" mock_container.attrs = { "State": {"StartedAt": "2025-01-01T00:00:00.000000000Z"}, } mock_docker_client = MagicMock() mock_docker_client.containers.get.return_value = mock_container # Create mock docker module with errors submodule mock_docker = MagicMock() mock_docker.from_env.return_value = mock_docker_client mock_docker_errors = MagicMock() mock_docker_errors.NotFound = type("NotFound", (Exception,), {}) with ( patch("src.server.api_routes.mcp_api.get_mcp_monitoring_config") as mock_get_config, patch.dict("sys.modules", {"docker": mock_docker, "docker.errors": mock_docker_errors}), ): mock_get_config.return_value = MCPMonitoringConfig(enable_docker_socket=True, health_check_timeout=5) result = await get_container_status() assert result["status"] == "running" mock_docker_client.containers.get.assert_called_once_with("archon-mcp") mock_docker_client.close.assert_called_once() # Environment Variable Tests @pytest.mark.asyncio async def test_config_defaults_to_http_mode(): """Test that configuration defaults to secure HTTP mode.""" # Clear any environment variables os.environ.pop("ENABLE_DOCKER_SOCKET_MONITORING", None) os.environ.pop("MCP_HEALTH_CHECK_TIMEOUT", None) from src.server.config.config import get_mcp_monitoring_config config = get_mcp_monitoring_config() assert config.enable_docker_socket is False assert config.health_check_timeout == 5 @pytest.mark.asyncio async def test_config_respects_environment_variables(): """Test that configuration respects environment variables.""" os.environ["ENABLE_DOCKER_SOCKET_MONITORING"] = "true" os.environ["MCP_HEALTH_CHECK_TIMEOUT"] = "10" from src.server.config.config import get_mcp_monitoring_config config = get_mcp_monitoring_config() assert config.enable_docker_socket is True assert config.health_check_timeout == 10 # Cleanup os.environ.pop("ENABLE_DOCKER_SOCKET_MONITORING", None) os.environ.pop("MCP_HEALTH_CHECK_TIMEOUT", None)