""" Tests for FastAPI endpoints. Tests health check and MCP JSON-RPC endpoints. """ from unittest.mock import AsyncMock, MagicMock, patch import pytest class TestHealthEndpoint: """Tests for health check endpoint.""" @pytest.mark.asyncio async def test_health_no_providers(self): """Test health check when no providers configured.""" from httpx import ASGITransport, AsyncClient from server import app with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", None), patch("server._github_provider", None), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] in ["healthy", "degraded"] assert data["service"] == "git-ops" @pytest.mark.asyncio async def test_health_with_gitea_connected(self): """Test health check with Gitea provider connected.""" from httpx import ASGITransport, AsyncClient from server import app mock_gitea = AsyncMock() mock_gitea.is_connected = AsyncMock(return_value=True) mock_gitea.get_authenticated_user = AsyncMock(return_value="test-user") with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", mock_gitea), patch("server._github_provider", None), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert "gitea" in data["dependencies"] @pytest.mark.asyncio async def test_health_with_gitea_not_connected(self): """Test health check when Gitea is not connected.""" from httpx import ASGITransport, AsyncClient from server import app mock_gitea = AsyncMock() mock_gitea.is_connected = AsyncMock(return_value=False) with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", mock_gitea), patch("server._github_provider", None), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "degraded" @pytest.mark.asyncio async def test_health_with_gitea_error(self): """Test health check when Gitea throws error.""" from httpx import ASGITransport, AsyncClient from server import app mock_gitea = AsyncMock() mock_gitea.is_connected = AsyncMock(side_effect=Exception("Connection failed")) with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", mock_gitea), patch("server._github_provider", None), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "degraded" @pytest.mark.asyncio async def test_health_with_github_connected(self): """Test health check with GitHub provider connected.""" from httpx import ASGITransport, AsyncClient from server import app mock_github = AsyncMock() mock_github.is_connected = AsyncMock(return_value=True) mock_github.get_authenticated_user = AsyncMock(return_value="github-user") with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", None), patch("server._github_provider", mock_github), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert "github" in data["dependencies"] @pytest.mark.asyncio async def test_health_with_github_not_connected(self): """Test health check when GitHub is not connected.""" from httpx import ASGITransport, AsyncClient from server import app mock_github = AsyncMock() mock_github.is_connected = AsyncMock(return_value=False) with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", None), patch("server._github_provider", mock_github), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "degraded" @pytest.mark.asyncio async def test_health_with_github_error(self): """Test health check when GitHub throws error.""" from httpx import ASGITransport, AsyncClient from server import app mock_github = AsyncMock() mock_github.is_connected = AsyncMock(side_effect=Exception("Auth failed")) with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", None), patch("server._gitea_provider", None), patch("server._github_provider", mock_github), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "degraded" @pytest.mark.asyncio async def test_health_with_workspace_manager(self): """Test health check with workspace manager.""" from pathlib import Path from httpx import ASGITransport, AsyncClient from server import app mock_manager = AsyncMock() mock_manager.base_path = Path("/tmp/workspaces") mock_manager.list_workspaces = AsyncMock(return_value=[]) with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", mock_manager), patch("server._gitea_provider", None), patch("server._github_provider", None), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert "workspace" in data["dependencies"] @pytest.mark.asyncio async def test_health_workspace_error(self): """Test health check when workspace manager throws error.""" from pathlib import Path from httpx import ASGITransport, AsyncClient from server import app mock_manager = AsyncMock() mock_manager.base_path = Path("/tmp/workspaces") mock_manager.list_workspaces = AsyncMock(side_effect=Exception("Disk full")) with ( patch("server._settings", MagicMock()), patch("server._workspace_manager", mock_manager), patch("server._gitea_provider", None), patch("server._github_provider", None), ): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/health") assert response.status_code == 200 data = response.json() assert data["status"] == "degraded" class TestMCPToolsEndpoint: """Tests for MCP tools list endpoint.""" @pytest.mark.asyncio async def test_list_mcp_tools(self): """Test listing MCP tools.""" from httpx import ASGITransport, AsyncClient from server import app with patch("server._settings", MagicMock()): async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.get("/mcp/tools") assert response.status_code == 200 data = response.json() assert "tools" in data class TestMCPRPCEndpoint: """Tests for MCP JSON-RPC endpoint.""" @pytest.mark.asyncio async def test_mcp_rpc_invalid_json(self): """Test RPC with invalid JSON.""" from httpx import ASGITransport, AsyncClient from server import app async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.post( "/mcp", content="not valid json", headers={"Content-Type": "application/json"}, ) assert response.status_code == 400 data = response.json() assert data["error"]["code"] == -32700 @pytest.mark.asyncio async def test_mcp_rpc_invalid_jsonrpc(self): """Test RPC with invalid jsonrpc version.""" from httpx import ASGITransport, AsyncClient from server import app async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.post( "/mcp", json={"jsonrpc": "1.0", "method": "test", "id": 1} ) assert response.status_code == 400 data = response.json() assert data["error"]["code"] == -32600 @pytest.mark.asyncio async def test_mcp_rpc_missing_method(self): """Test RPC with missing method.""" from httpx import ASGITransport, AsyncClient from server import app async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.post("/mcp", json={"jsonrpc": "2.0", "id": 1}) assert response.status_code == 400 data = response.json() assert data["error"]["code"] == -32600 @pytest.mark.asyncio async def test_mcp_rpc_method_not_found(self): """Test RPC with unknown method.""" from httpx import ASGITransport, AsyncClient from server import app async with AsyncClient( transport=ASGITransport(app=app), base_url="http://test" ) as client: response = await client.post( "/mcp", json={ "jsonrpc": "2.0", "method": "unknown_method", "params": {}, "id": 1, }, ) assert response.status_code == 404 data = response.json() assert data["error"]["code"] == -32601 class TestTypeSchemaConversion: """Tests for type to JSON schema conversion.""" def test_python_type_to_json_schema_str(self): """Test converting str type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(str) assert result == {"type": "string"} def test_python_type_to_json_schema_int(self): """Test converting int type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(int) assert result == {"type": "integer"} def test_python_type_to_json_schema_float(self): """Test converting float type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(float) assert result == {"type": "number"} def test_python_type_to_json_schema_bool(self): """Test converting bool type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(bool) assert result == {"type": "boolean"} def test_python_type_to_json_schema_none(self): """Test converting NoneType to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(type(None)) assert result == {"type": "null"} def test_python_type_to_json_schema_list(self): """Test converting list type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(list[str]) assert result["type"] == "array" def test_python_type_to_json_schema_dict(self): """Test converting dict type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(dict[str, int]) assert result == {"type": "object"} def test_python_type_to_json_schema_optional(self): """Test converting Optional type to JSON schema.""" from server import _python_type_to_json_schema result = _python_type_to_json_schema(str | None) # The function returns object type for complex union types assert "type" in result class TestToolSchema: """Tests for tool schema extraction.""" def test_get_tool_schema_simple(self): """Test getting schema from simple function.""" from server import _get_tool_schema def simple_func(name: str, count: int) -> str: return f"{name}: {count}" result = _get_tool_schema(simple_func) assert "properties" in result assert "name" in result["properties"] assert "count" in result["properties"] def test_register_and_get_tool(self): """Test registering a tool.""" from server import _register_tool, _tool_registry async def test_tool(x: str) -> str: """A test tool.""" return x _register_tool("test_tool", test_tool, "Test description") assert "test_tool" in _tool_registry assert _tool_registry["test_tool"]["description"] == "Test description" # Clean up del _tool_registry["test_tool"]