Archon/python/tests/mcp_server/features/test_feature_tools.py
Rasmus Widing ed6479b4c3 refactor(mcp): Apply consistent error handling to all MCP tools
Comprehensive update to MCP server error handling:

Error Handling Improvements:
- Applied MCPErrorFormatter to all remaining MCP tool files
- Replaced all hardcoded timeout values with configurable timeout system
- Converted all simple string errors to structured error format
- Added proper httpx exception handling with detailed context

Tools Updated:
- document_tools.py: All 5 document management tools
- version_tools.py: All 4 version management tools
- feature_tools.py: Project features tool
- project_tools.py: Remaining 3 project tools (get, list, delete)
- task_tools.py: Remaining 4 task tools (get, list, update, delete)

Test Improvements:
- Removed backward compatibility checks from all tests
- Tests now enforce structured error format (dict not string)
- Any string error response is now considered a bug
- All 20 tests passing with new strict validation

This completes the error handling refactor for all MCP tools,
ensuring consistent client experience and better debugging.
2025-08-19 16:07:07 +03:00

127 lines
No EOL
4.6 KiB
Python

"""Unit tests for feature management tools."""
import json
from unittest.mock import AsyncMock, MagicMock, patch
import pytest
from mcp.server.fastmcp import Context
from src.mcp_server.features.feature_tools import register_feature_tools
@pytest.fixture
def mock_mcp():
"""Create a mock MCP server for testing."""
mock = MagicMock()
# Store registered tools
mock._tools = {}
def tool_decorator():
def decorator(func):
mock._tools[func.__name__] = func
return func
return decorator
mock.tool = tool_decorator
return mock
@pytest.fixture
def mock_context():
"""Create a mock context for testing."""
return MagicMock(spec=Context)
@pytest.mark.asyncio
async def test_get_project_features_success(mock_mcp, mock_context):
"""Test successful retrieval of project features."""
register_feature_tools(mock_mcp)
# Get the get_project_features function
get_project_features = mock_mcp._tools.get('get_project_features')
assert get_project_features is not None, "get_project_features tool not registered"
# Mock HTTP response with various feature structures
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {
"features": [
{"name": "authentication", "status": "completed", "components": ["oauth", "jwt"]},
{"name": "api", "status": "in_progress", "endpoints_done": 12, "endpoints_total": 20},
{"name": "database", "status": "planned"},
{"name": "payments", "provider": "stripe", "version": "2.0", "enabled": True}
]
}
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
mock_async_client = AsyncMock()
mock_async_client.get.return_value = mock_response
mock_client.return_value.__aenter__.return_value = mock_async_client
result = await get_project_features(mock_context, project_id="project-123")
result_data = json.loads(result)
assert result_data["success"] is True
assert result_data["count"] == 4
assert len(result_data["features"]) == 4
# Verify different feature structures are preserved
features = result_data["features"]
assert features[0]["components"] == ["oauth", "jwt"]
assert features[1]["endpoints_done"] == 12
assert features[2]["status"] == "planned"
assert features[3]["provider"] == "stripe"
@pytest.mark.asyncio
async def test_get_project_features_empty(mock_mcp, mock_context):
"""Test getting features for a project with no features defined."""
register_feature_tools(mock_mcp)
get_project_features = mock_mcp._tools.get('get_project_features')
# Mock response with empty features
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"features": []}
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
mock_async_client = AsyncMock()
mock_async_client.get.return_value = mock_response
mock_client.return_value.__aenter__.return_value = mock_async_client
result = await get_project_features(mock_context, project_id="project-123")
result_data = json.loads(result)
assert result_data["success"] is True
assert result_data["count"] == 0
assert result_data["features"] == []
@pytest.mark.asyncio
async def test_get_project_features_not_found(mock_mcp, mock_context):
"""Test getting features for a non-existent project."""
register_feature_tools(mock_mcp)
get_project_features = mock_mcp._tools.get('get_project_features')
# Mock 404 response
mock_response = MagicMock()
mock_response.status_code = 404
mock_response.text = "Project not found"
with patch('src.mcp_server.features.feature_tools.httpx.AsyncClient') as mock_client:
mock_async_client = AsyncMock()
mock_async_client.get.return_value = mock_response
mock_client.return_value.__aenter__.return_value = mock_async_client
result = await get_project_features(mock_context, project_id="non-existent")
result_data = json.loads(result)
assert result_data["success"] is False
# Error must be structured format (dict), not string
assert "error" in result_data
assert isinstance(result_data["error"], dict), "Error should be structured format, not string"
assert result_data["error"]["type"] == "not_found"
assert "not found" in result_data["error"]["message"].lower()