generated from personal-projects/leo-claude-mktplace
test: add comprehensive test suite - Closes #7
Added comprehensive test coverage for all tool modules: Test Files Created: - tests/conftest.py: Shared fixtures for all tests - tests/test_issues.py: Complete coverage for issue tools - tests/test_labels.py: Complete coverage for label tools - tests/test_milestones.py: Complete coverage for milestone tools Test Coverage: - Tool definition validation (schema structure) - Handler function routing - Successful API response formatting - Error handling (GiteaClientError) - Required parameter validation - Optional parameter handling - Mock Gitea API responses Configuration Updates: - Added pytest-cov>=4.0.0 to dev dependencies - Created run_tests.sh script for easy test execution All tests use pytest-asyncio for async functions and mock the GiteaClient to avoid real API calls. Tests validate tool schemas, handler routing, response formatting, and error handling. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
334
tests/test_milestones.py
Normal file
334
tests/test_milestones.py
Normal file
@@ -0,0 +1,334 @@
|
||||
"""Tests for milestone operations tools."""
|
||||
|
||||
import pytest
|
||||
from mcp.types import Tool, TextContent
|
||||
from gitea_mcp.tools.milestones import (
|
||||
get_milestone_tools,
|
||||
handle_milestone_tool,
|
||||
_list_milestones,
|
||||
_create_milestone,
|
||||
)
|
||||
from gitea_mcp.client import GiteaClientError
|
||||
|
||||
|
||||
class TestMilestoneToolDefinitions:
|
||||
"""Test milestone tool schema definitions."""
|
||||
|
||||
def test_get_milestone_tools_returns_list(self):
|
||||
"""Test that get_milestone_tools returns a list of Tool objects."""
|
||||
tools = get_milestone_tools()
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) == 2
|
||||
assert all(isinstance(tool, Tool) for tool in tools)
|
||||
|
||||
def test_list_milestones_tool_schema(self):
|
||||
"""Test gitea_list_milestones tool has correct schema."""
|
||||
tools = get_milestone_tools()
|
||||
list_tool = next(t for t in tools if t.name == "gitea_list_milestones")
|
||||
|
||||
assert list_tool.name == "gitea_list_milestones"
|
||||
assert "list" in list_tool.description.lower()
|
||||
assert "milestone" in list_tool.description.lower()
|
||||
|
||||
schema = list_tool.inputSchema
|
||||
assert schema["type"] == "object"
|
||||
assert set(schema["required"]) == {"owner", "repo"}
|
||||
assert "owner" in schema["properties"]
|
||||
assert "repo" in schema["properties"]
|
||||
assert "state" in schema["properties"]
|
||||
assert schema["properties"]["state"]["enum"] == ["open", "closed", "all"]
|
||||
|
||||
def test_create_milestone_tool_schema(self):
|
||||
"""Test gitea_create_milestone tool has correct schema."""
|
||||
tools = get_milestone_tools()
|
||||
create_tool = next(t for t in tools if t.name == "gitea_create_milestone")
|
||||
|
||||
assert create_tool.name == "gitea_create_milestone"
|
||||
assert "create" in create_tool.description.lower()
|
||||
|
||||
schema = create_tool.inputSchema
|
||||
assert set(schema["required"]) == {"owner", "repo", "title"}
|
||||
assert "title" in schema["properties"]
|
||||
assert "description" in schema["properties"]
|
||||
assert "due_on" in schema["properties"]
|
||||
assert "iso 8601" in schema["properties"]["due_on"]["description"].lower()
|
||||
|
||||
|
||||
class TestListMilestones:
|
||||
"""Test _list_milestones function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_milestones_success(self, mock_client, sample_milestone):
|
||||
"""Test successful milestone listing."""
|
||||
additional_milestone = {
|
||||
"id": 11,
|
||||
"title": "v2.0",
|
||||
"description": "Second major release",
|
||||
"state": "open",
|
||||
"open_issues": 10,
|
||||
"closed_issues": 0,
|
||||
"created_at": "2024-02-01T00:00:00Z",
|
||||
"updated_at": "2024-02-01T00:00:00Z",
|
||||
}
|
||||
mock_client.get.return_value = [sample_milestone, additional_milestone]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"state": "open",
|
||||
}
|
||||
|
||||
result = await _list_milestones(arguments, mock_client)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert "v1.0" in result[0].text
|
||||
assert "v2.0" in result[0].text
|
||||
assert "First major release" in result[0].text
|
||||
assert "Open Issues: 5" in result[0].text
|
||||
assert "Closed Issues: 15" in result[0].text
|
||||
|
||||
mock_client.get.assert_called_once()
|
||||
call_args = mock_client.get.call_args
|
||||
assert "/repos/testowner/testrepo/milestones" in call_args[0][0]
|
||||
assert call_args[1]["params"]["state"] == "open"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_milestones_empty(self, mock_client):
|
||||
"""Test listing when no milestones found."""
|
||||
mock_client.get.return_value = []
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"state": "closed",
|
||||
}
|
||||
|
||||
result = await _list_milestones(arguments, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "No closed milestones found" in result[0].text
|
||||
assert "testowner/testrepo" in result[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_milestones_default_state(self, mock_client, sample_milestone):
|
||||
"""Test listing milestones with default state."""
|
||||
mock_client.get.return_value = [sample_milestone]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await _list_milestones(arguments, mock_client)
|
||||
|
||||
call_args = mock_client.get.call_args
|
||||
assert call_args[1]["params"]["state"] == "open"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_milestones_all_states(self, mock_client, sample_milestone):
|
||||
"""Test listing milestones with state=all."""
|
||||
mock_client.get.return_value = [sample_milestone]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"state": "all",
|
||||
}
|
||||
|
||||
result = await _list_milestones(arguments, mock_client)
|
||||
|
||||
assert "Found 1 all milestone(s)" in result[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_milestones_with_due_date(self, mock_client, sample_milestone):
|
||||
"""Test milestone listing includes due date."""
|
||||
mock_client.get.return_value = [sample_milestone]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await _list_milestones(arguments, mock_client)
|
||||
|
||||
assert "Due: 2024-12-31T23:59:59Z" in result[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_milestones_without_optional_fields(self, mock_client):
|
||||
"""Test milestone listing without description and due date."""
|
||||
minimal_milestone = {
|
||||
"id": 10,
|
||||
"title": "v1.0",
|
||||
"state": "open",
|
||||
"open_issues": 5,
|
||||
"closed_issues": 15,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
}
|
||||
mock_client.get.return_value = [minimal_milestone]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await _list_milestones(arguments, mock_client)
|
||||
|
||||
assert "v1.0" in result[0].text
|
||||
assert "State: open" in result[0].text
|
||||
assert "Created: 2024-01-01T00:00:00Z" in result[0].text
|
||||
|
||||
|
||||
class TestCreateMilestone:
|
||||
"""Test _create_milestone function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_milestone_minimal(self, mock_client, sample_milestone):
|
||||
"""Test creating milestone with minimal required fields."""
|
||||
mock_client.post.return_value = sample_milestone
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"title": "v1.0",
|
||||
}
|
||||
|
||||
result = await _create_milestone(arguments, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Created milestone: v1.0" in result[0].text
|
||||
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
assert "/repos/testowner/testrepo/milestones" in call_args[0][0]
|
||||
json_data = call_args[1]["json"]
|
||||
assert json_data["title"] == "v1.0"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_milestone_with_description(self, mock_client, sample_milestone):
|
||||
"""Test creating milestone with description."""
|
||||
mock_client.post.return_value = sample_milestone
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"title": "v1.0",
|
||||
"description": "First major release",
|
||||
}
|
||||
|
||||
result = await _create_milestone(arguments, mock_client)
|
||||
|
||||
assert "Description: First major release" in result[0].text
|
||||
|
||||
call_args = mock_client.post.call_args
|
||||
json_data = call_args[1]["json"]
|
||||
assert json_data["description"] == "First major release"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_milestone_with_due_date(self, mock_client, sample_milestone):
|
||||
"""Test creating milestone with due date."""
|
||||
mock_client.post.return_value = sample_milestone
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"title": "v1.0",
|
||||
"due_on": "2024-12-31T23:59:59Z",
|
||||
}
|
||||
|
||||
result = await _create_milestone(arguments, mock_client)
|
||||
|
||||
assert "Due: 2024-12-31T23:59:59Z" in result[0].text
|
||||
|
||||
call_args = mock_client.post.call_args
|
||||
json_data = call_args[1]["json"]
|
||||
assert json_data["due_on"] == "2024-12-31T23:59:59Z"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_milestone_full(self, mock_client, sample_milestone):
|
||||
"""Test creating milestone with all optional fields."""
|
||||
mock_client.post.return_value = sample_milestone
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"title": "v1.0",
|
||||
"description": "First major release",
|
||||
"due_on": "2024-12-31T23:59:59Z",
|
||||
}
|
||||
|
||||
result = await _create_milestone(arguments, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Created milestone: v1.0" in result[0].text
|
||||
assert "Description: First major release" in result[0].text
|
||||
assert "Due: 2024-12-31T23:59:59Z" in result[0].text
|
||||
|
||||
call_args = mock_client.post.call_args
|
||||
json_data = call_args[1]["json"]
|
||||
assert json_data["title"] == "v1.0"
|
||||
assert json_data["description"] == "First major release"
|
||||
assert json_data["due_on"] == "2024-12-31T23:59:59Z"
|
||||
|
||||
|
||||
class TestMilestoneToolHandler:
|
||||
"""Test handle_milestone_tool function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_list_milestones(self, mock_client, sample_milestone):
|
||||
"""Test handler routes to _list_milestones."""
|
||||
mock_client.get.return_value = [sample_milestone]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await handle_milestone_tool("gitea_list_milestones", arguments, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert "v1.0" in result[0].text
|
||||
mock_client.get.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_create_milestone(self, mock_client, sample_milestone):
|
||||
"""Test handler routes to _create_milestone."""
|
||||
mock_client.post.return_value = sample_milestone
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"title": "v1.0",
|
||||
}
|
||||
|
||||
result = await handle_milestone_tool("gitea_create_milestone", arguments, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Created milestone" in result[0].text
|
||||
mock_client.post.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_unknown_tool(self, mock_client):
|
||||
"""Test handler with unknown tool name."""
|
||||
result = await handle_milestone_tool("gitea_unknown_tool", {}, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Unknown milestone tool" in result[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_client_error(self, mock_client):
|
||||
"""Test handler gracefully handles GiteaClientError."""
|
||||
mock_client.get.side_effect = GiteaClientError("API Error")
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await handle_milestone_tool("gitea_list_milestones", arguments, mock_client)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Error:" in result[0].text
|
||||
assert "API Error" in result[0].text
|
||||
Reference in New Issue
Block a user