Skip to main content
Glama
test_dashboard_api.py13.6 kB
"""Tests for dashboard REST API endpoints using Starlette TestClient.""" import pytest from starlette.testclient import TestClient from mcp_server_browser_use.server import serve @pytest.fixture def anyio_backend(): return "asyncio" @pytest.fixture def client(monkeypatch): """Create a synchronous HTTP client for the FastMCP server.""" # Set environment variables for testing monkeypatch.setenv("MCP_LLM_PROVIDER", "openai") monkeypatch.setenv("MCP_LLM_MODEL_NAME", "gpt-4") monkeypatch.setenv("OPENAI_API_KEY", "test-key") monkeypatch.setenv("MCP_BROWSER_HEADLESS", "true") monkeypatch.setenv("MCP_SKILLS_ENABLED", "true") # Reload config module to pick up new env vars import importlib import mcp_server_browser_use.config importlib.reload(mcp_server_browser_use.config) # Update settings reference in server module before reloading import mcp_server_browser_use.server mcp_server_browser_use.server.settings = mcp_server_browser_use.config.settings importlib.reload(mcp_server_browser_use.server) from mcp_server_browser_use.server import serve server = serve() # Create Starlette TestClient for the ASGI app return TestClient(server.http_app()) @pytest.fixture def client_skills_disabled(monkeypatch): """Create a synchronous HTTP client with skills disabled.""" # Set environment variables for testing monkeypatch.setenv("MCP_LLM_PROVIDER", "openai") monkeypatch.setenv("MCP_LLM_MODEL_NAME", "gpt-4") monkeypatch.setenv("OPENAI_API_KEY", "test-key") monkeypatch.setenv("MCP_BROWSER_HEADLESS", "true") # Reload config module to pick up new env vars import importlib import mcp_server_browser_use.config importlib.reload(mcp_server_browser_use.config) # Directly disable skills in the loaded settings mcp_server_browser_use.config.settings.skills.enabled = False # Update settings reference in server module before reloading import mcp_server_browser_use.server mcp_server_browser_use.server.settings = mcp_server_browser_use.config.settings importlib.reload(mcp_server_browser_use.server) from mcp_server_browser_use.server import serve server = serve() # Create Starlette TestClient for the ASGI app return TestClient(server.http_app()) class TestHealthEndpoint: """Test /api/health endpoint.""" def test_health_check_returns_ok(self, client): """Should return healthy status with server stats.""" response = client.get("/api/health") assert response.status_code == 200 data = response.json() assert data["status"] == "healthy" assert "uptime_seconds" in data assert "memory_mb" in data assert "running_tasks" in data assert "stats" in data def test_health_check_response_structure(self, client): """Should have proper response structure.""" response = client.get("/api/health") data = response.json() assert isinstance(data["uptime_seconds"], (int, float)) assert isinstance(data["memory_mb"], (int, float)) assert isinstance(data["running_tasks"], int) assert isinstance(data["tasks"], list) assert data["uptime_seconds"] >= 0 assert data["memory_mb"] > 0 class TestTaskListEndpoint: """Test /api/tasks endpoint.""" def test_task_list_returns_empty_initially(self, client): """Should return empty task list initially.""" response = client.get("/api/tasks") assert response.status_code == 200 data = response.json() assert "tasks" in data assert "count" in data assert isinstance(data["tasks"], list) def test_task_list_with_limit_parameter(self, client): """Should accept limit parameter.""" response = client.get("/api/tasks?limit=5") assert response.status_code == 200 data = response.json() assert isinstance(data["tasks"], list) assert len(data["tasks"]) <= 5 def test_task_list_with_status_filter_valid(self, client): """Should filter by valid status.""" response = client.get("/api/tasks?status=running") assert response.status_code == 200 assert "tasks" in response.json() @pytest.mark.parametrize("status", ["running", "completed", "failed", "pending"]) def test_task_list_all_valid_statuses(self, client, status): """Should accept all valid status values.""" response = client.get(f"/api/tasks?status={status}") assert response.status_code == 200 assert "tasks" in response.json() class TestTaskGetEndpoint: """Test /api/tasks/{task_id} endpoint.""" def test_task_get_not_found(self, client): """Should return 404 for non-existent task.""" response = client.get("/api/tasks/nonexistent") assert response.status_code == 404 class TestDashboardHtmlEndpoint: """Test /dashboard endpoint.""" def test_dashboard_serves_html(self, client): """Should serve dashboard HTML file.""" response = client.get("/dashboard") assert response.status_code == 200 assert "text/html" in response.headers.get("content-type", "") assert response.text # Should have content def test_dashboard_html_is_valid(self, client): """Dashboard HTML should be valid markup.""" response = client.get("/dashboard") content = response.text # Basic checks for HTML structure assert "<html" in content.lower() or "<!doctype" in content.lower() class TestViewerHtmlEndpoint: """Test / endpoint.""" def test_viewer_serves_html(self, client): """Should serve viewer HTML from root.""" response = client.get("/") assert response.status_code == 200 assert "text/html" in response.headers.get("content-type", "") assert response.text # Should have content def test_viewer_html_is_valid(self, client): """Viewer HTML should be valid markup.""" response = client.get("/") content = response.text # Basic checks for HTML structure assert "<html" in content.lower() or "<!doctype" in content.lower() class TestSkillsListEndpoint: """Test /api/skills endpoint.""" def test_skills_list_when_enabled(self, client): """Should return skills list when feature is enabled.""" response = client.get("/api/skills") assert response.status_code == 200 data = response.json() assert "skills" in data assert "count" in data assert "skills_directory" in data assert isinstance(data["skills"], list) def test_skills_list_disabled_returns_error(self, client_skills_disabled): """Should return error when skills feature is disabled.""" response = client_skills_disabled.get("/api/skills") assert response.status_code == 503 def test_skills_list_structure(self, client): """Skills list should have proper structure.""" response = client.get("/api/skills") data = response.json() for skill in data["skills"]: assert "name" in skill assert "description" in skill assert "success_rate" in skill assert "usage_count" in skill class TestSkillGetEndpoint: """Test /api/skills/{name} GET endpoint.""" def test_skill_get_not_found(self, client): """Should return 404 for non-existent skill.""" response = client.get("/api/skills/nonexistent_skill") assert response.status_code == 404 def test_skill_get_disabled_returns_error(self, client_skills_disabled): """Should return error when skills feature is disabled.""" response = client_skills_disabled.get("/api/skills/any_skill") assert response.status_code == 503 class TestSkillDeleteEndpoint: """Test /api/skills/{name} DELETE endpoint.""" def test_skill_delete_not_found(self, client): """Should return 404 when skill doesn't exist.""" response = client.delete("/api/skills/nonexistent_skill") assert response.status_code == 404 def test_skill_delete_disabled_returns_error(self, client_skills_disabled): """Should return error when skills feature is disabled.""" response = client_skills_disabled.delete("/api/skills/any_skill") assert response.status_code == 503 class TestSkillRunEndpoint: """Test /api/skills/{name}/run POST endpoint.""" def test_skill_run_success_with_minimal_params(self, client): """Should successfully start skill execution with minimal params.""" response = client.post("/api/skills/test_skill/run", json={}) # Should return 202 (Accepted) for async task or 404 if skill not found assert response.status_code in (202, 404) if response.status_code == 202: data = response.json() assert "task_id" in data assert "message" in data def test_skill_run_with_url_and_params(self, client): """Should accept URL and params in request body.""" payload = { "url": "https://example.com", "params": {"param1": "value1"}, } response = client.post("/api/skills/test_skill/run", json=payload) assert response.status_code in (202, 404) def test_skill_run_disabled_returns_error(self, client_skills_disabled): """Should return error when skills feature is disabled.""" response = client_skills_disabled.post("/api/skills/test_skill/run", json={}) assert response.status_code == 503 class TestLearnEndpoint: """Test /api/learn POST endpoint.""" def test_learn_missing_task(self, client): """Should require task field in request.""" response = client.post("/api/learn", json={}) assert response.status_code == 400 def test_learn_success_with_task_only(self, client): """Should successfully start learning session with task only.""" payload = {"task": "Learn to search GitHub"} response = client.post("/api/learn", json=payload) assert response.status_code == 202 data = response.json() assert "task_id" in data assert "learning_task" in data def test_learn_success_with_skill_name(self, client): """Should accept optional skill_name parameter.""" payload = { "task": "Learn to search GitHub", "skill_name": "github_search", } response = client.post("/api/learn", json=payload) assert response.status_code == 202 data = response.json() assert "task_id" in data assert data["skill_name"] == "github_search" def test_learn_disabled_returns_error(self, client_skills_disabled): """Should return error when skills feature is disabled.""" payload = {"task": "Test task"} response = client_skills_disabled.post("/api/learn", json=payload) assert response.status_code == 503 class TestEventsStreamEndpoint: """Test /api/events SSE endpoint.""" @pytest.mark.skip(reason="SSE endpoints stream indefinitely and block TestClient") def test_events_stream_connection(self, client): """Should establish SSE connection and return event stream.""" with client.stream("GET", "/api/events") as response: assert response.status_code == 200 assert response.headers.get("content-type") == "text/event-stream" @pytest.mark.skip(reason="SSE endpoints stream indefinitely and block TestClient") def test_events_stream_headers(self, client): """Should have proper SSE headers.""" with client.stream("GET", "/api/events") as response: headers = response.headers assert headers.get("content-type") == "text/event-stream" assert headers.get("cache-control") == "no-cache" assert headers.get("connection") == "keep-alive" class TestTaskLogsStreamEndpoint: """Test /api/tasks/{task_id}/logs SSE endpoint.""" def test_task_logs_stream_not_found(self, client): """Should return 404 for non-existent task.""" response = client.get("/api/tasks/nonexistent/logs") assert response.status_code == 404 class TestApiResponseConsistency: """Test consistency of API responses.""" def test_json_response_format(self, client): """API endpoints should return valid JSON.""" response = client.get("/api/health") assert response.status_code == 200 # Should be parseable as JSON data = response.json() assert isinstance(data, dict) def test_error_response_format(self, client): """Error responses should be consistent.""" response = client.get("/api/tasks/nonexistent") assert response.status_code == 404 data = response.json() assert "error" in data def test_api_endpoints_exist(self, client): """Core API endpoints should exist.""" endpoints = [ ("/api/health", "get"), ("/api/tasks", "get"), ("/api/skills", "get"), ("/dashboard", "get"), ("/", "get"), ] for path, method in endpoints: if method == "get": response = client.get(path) # Should not be 404 (Not Found) - endpoint should exist assert response.status_code != 404, f"{path} endpoint not found" # Note: /api/events is SSE streaming endpoint, tested separately in TestEventsStreamEndpoint

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/Saik0s/mcp-browser-use'

If you have feedback or need assistance with the MCP directory API, please join our Discord server