forked from cardosofelipe/fast-next-template
**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:
440
mcp-servers/git-ops/tests/test_api_endpoints.py
Normal file
440
mcp-servers/git-ops/tests/test_api_endpoints.py
Normal 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"]
|
||||
1170
mcp-servers/git-ops/tests/test_server_tools.py
Normal file
1170
mcp-servers/git-ops/tests/test_server_tools.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user