**test(git-ops): add comprehensive tests for server and API tools**

- Introduced extensive test coverage for FastAPI endpoints, including health check, MCP tools, and JSON-RPC operations.
- Added tests for Git operations MCP tools, including cloning, status, branching, committing, and provider detection.
- Mocked dependencies and ensured reliable test isolation with unittest.mock and pytest fixtures.
- Validated error handling, workspace management, tool execution, and type conversion functions.
This commit is contained in:
2026-01-07 09:17:32 +01:00
parent 011b21bf0a
commit 0a624a94af
2 changed files with 1610 additions and 0 deletions

View File

@@ -0,0 +1,440 @@
"""
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"]

File diff suppressed because it is too large Load Diff