generated from personal-projects/leo-claude-mktplace
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>
373 lines
12 KiB
Python
373 lines
12 KiB
Python
"""Tests for issue operations tools."""
|
|
|
|
import pytest
|
|
from mcp.types import Tool, TextContent
|
|
from gitea_mcp.tools.issues import (
|
|
get_issue_tools,
|
|
handle_issue_tool,
|
|
_list_issues,
|
|
_get_issue,
|
|
_create_issue,
|
|
_update_issue,
|
|
)
|
|
from gitea_mcp.client import GiteaClientError
|
|
|
|
|
|
class TestIssueToolDefinitions:
|
|
"""Test issue tool schema definitions."""
|
|
|
|
def test_get_issue_tools_returns_list(self):
|
|
"""Test that get_issue_tools returns a list of Tool objects."""
|
|
tools = get_issue_tools()
|
|
assert isinstance(tools, list)
|
|
assert len(tools) == 4
|
|
assert all(isinstance(tool, Tool) for tool in tools)
|
|
|
|
def test_list_issues_tool_schema(self):
|
|
"""Test gitea_list_issues tool has correct schema."""
|
|
tools = get_issue_tools()
|
|
list_tool = next(t for t in tools if t.name == "gitea_list_issues")
|
|
|
|
assert list_tool.name == "gitea_list_issues"
|
|
assert "list issues" 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 "labels" in schema["properties"]
|
|
assert "milestone" in schema["properties"]
|
|
|
|
def test_get_issue_tool_schema(self):
|
|
"""Test gitea_get_issue tool has correct schema."""
|
|
tools = get_issue_tools()
|
|
get_tool = next(t for t in tools if t.name == "gitea_get_issue")
|
|
|
|
assert get_tool.name == "gitea_get_issue"
|
|
assert "get details" in get_tool.description.lower()
|
|
|
|
schema = get_tool.inputSchema
|
|
assert set(schema["required"]) == {"owner", "repo", "index"}
|
|
assert "index" in schema["properties"]
|
|
|
|
def test_create_issue_tool_schema(self):
|
|
"""Test gitea_create_issue tool has correct schema."""
|
|
tools = get_issue_tools()
|
|
create_tool = next(t for t in tools if t.name == "gitea_create_issue")
|
|
|
|
assert create_tool.name == "gitea_create_issue"
|
|
assert "create" in create_tool.description.lower()
|
|
|
|
schema = create_tool.inputSchema
|
|
assert set(schema["required"]) == {"owner", "repo", "title"}
|
|
assert "body" in schema["properties"]
|
|
assert "labels" in schema["properties"]
|
|
assert "assignees" in schema["properties"]
|
|
|
|
def test_update_issue_tool_schema(self):
|
|
"""Test gitea_update_issue tool has correct schema."""
|
|
tools = get_issue_tools()
|
|
update_tool = next(t for t in tools if t.name == "gitea_update_issue")
|
|
|
|
assert update_tool.name == "gitea_update_issue"
|
|
assert "update" in update_tool.description.lower()
|
|
|
|
schema = update_tool.inputSchema
|
|
assert set(schema["required"]) == {"owner", "repo", "index"}
|
|
assert "title" in schema["properties"]
|
|
assert "state" in schema["properties"]
|
|
|
|
|
|
class TestListIssues:
|
|
"""Test _list_issues function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_issues_success(self, mock_client, sample_issue):
|
|
"""Test successful issue listing."""
|
|
mock_client.get.return_value = [sample_issue]
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"state": "open",
|
|
}
|
|
|
|
result = await _list_issues(arguments, mock_client)
|
|
|
|
assert isinstance(result, list)
|
|
assert len(result) == 1
|
|
assert isinstance(result[0], TextContent)
|
|
assert "Test Issue" in result[0].text
|
|
assert "#42" in result[0].text
|
|
assert "bug" in result[0].text
|
|
|
|
mock_client.get.assert_called_once()
|
|
call_args = mock_client.get.call_args
|
|
assert "/repos/testowner/testrepo/issues" in call_args[0][0]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_issues_empty(self, mock_client):
|
|
"""Test listing when no issues found."""
|
|
mock_client.get.return_value = []
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"state": "closed",
|
|
}
|
|
|
|
result = await _list_issues(arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "No closed issues found" in result[0].text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_list_issues_with_filters(self, mock_client, sample_issue):
|
|
"""Test listing with label and milestone filters."""
|
|
mock_client.get.return_value = [sample_issue]
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"state": "all",
|
|
"labels": "bug,priority-high",
|
|
"milestone": "v1.0",
|
|
"page": 2,
|
|
"limit": 50,
|
|
}
|
|
|
|
result = await _list_issues(arguments, mock_client)
|
|
|
|
mock_client.get.assert_called_once()
|
|
call_args = mock_client.get.call_args
|
|
params = call_args[1]["params"]
|
|
assert params["labels"] == "bug,priority-high"
|
|
assert params["milestone"] == "v1.0"
|
|
assert params["page"] == 2
|
|
assert params["limit"] == 50
|
|
|
|
|
|
class TestGetIssue:
|
|
"""Test _get_issue function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_get_issue_success(self, mock_client, sample_issue):
|
|
"""Test successful issue retrieval."""
|
|
mock_client.get.return_value = sample_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"index": 42,
|
|
}
|
|
|
|
result = await _get_issue(arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Issue #42" in result[0].text
|
|
assert "Test Issue" in result[0].text
|
|
assert "This is a test issue" in result[0].text
|
|
assert "bug" in result[0].text
|
|
assert "v1.0" in result[0].text
|
|
assert "testuser" in result[0].text
|
|
|
|
mock_client.get.assert_called_once()
|
|
assert "/repos/testowner/testrepo/issues/42" in mock_client.get.call_args[0][0]
|
|
|
|
|
|
class TestCreateIssue:
|
|
"""Test _create_issue function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_issue_minimal(self, mock_client, sample_issue):
|
|
"""Test creating issue with minimal required fields."""
|
|
mock_client.post.return_value = sample_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"title": "New Issue",
|
|
}
|
|
|
|
result = await _create_issue(arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Created issue #42" in result[0].text
|
|
assert "Test Issue" in result[0].text
|
|
|
|
mock_client.post.assert_called_once()
|
|
call_args = mock_client.post.call_args
|
|
assert "/repos/testowner/testrepo/issues" in call_args[0][0]
|
|
assert call_args[1]["json"]["title"] == "New Issue"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_create_issue_full(self, mock_client, sample_issue):
|
|
"""Test creating issue with all optional fields."""
|
|
mock_client.post.return_value = sample_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"title": "New Issue",
|
|
"body": "Detailed description",
|
|
"labels": [1, 2],
|
|
"milestone": 10,
|
|
"assignees": ["user1", "user2"],
|
|
}
|
|
|
|
result = await _create_issue(arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
|
|
call_args = mock_client.post.call_args
|
|
json_data = call_args[1]["json"]
|
|
assert json_data["title"] == "New Issue"
|
|
assert json_data["body"] == "Detailed description"
|
|
assert json_data["labels"] == [1, 2]
|
|
assert json_data["milestone"] == 10
|
|
assert json_data["assignees"] == ["user1", "user2"]
|
|
|
|
|
|
class TestUpdateIssue:
|
|
"""Test _update_issue function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_issue_title(self, mock_client, sample_issue):
|
|
"""Test updating issue title."""
|
|
updated_issue = sample_issue.copy()
|
|
updated_issue["title"] = "Updated Title"
|
|
mock_client.patch.return_value = updated_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"index": 42,
|
|
"title": "Updated Title",
|
|
}
|
|
|
|
result = await _update_issue(arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Updated issue #42" in result[0].text
|
|
|
|
mock_client.patch.assert_called_once()
|
|
call_args = mock_client.patch.call_args
|
|
assert "/repos/testowner/testrepo/issues/42" in call_args[0][0]
|
|
assert call_args[1]["json"]["title"] == "Updated Title"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_update_issue_state(self, mock_client, sample_issue):
|
|
"""Test closing an issue."""
|
|
closed_issue = sample_issue.copy()
|
|
closed_issue["state"] = "closed"
|
|
mock_client.patch.return_value = closed_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"index": 42,
|
|
"state": "closed",
|
|
}
|
|
|
|
result = await _update_issue(arguments, mock_client)
|
|
|
|
assert "State: closed" in result[0].text
|
|
|
|
call_args = mock_client.patch.call_args
|
|
assert call_args[1]["json"]["state"] == "closed"
|
|
|
|
|
|
class TestIssueToolHandler:
|
|
"""Test handle_issue_tool function."""
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_list_issues(self, mock_client, sample_issue):
|
|
"""Test handler routes to _list_issues."""
|
|
mock_client.get.return_value = [sample_issue]
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
}
|
|
|
|
result = await handle_issue_tool("gitea_list_issues", arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert isinstance(result[0], TextContent)
|
|
mock_client.get.assert_called_once()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_get_issue(self, mock_client, sample_issue):
|
|
"""Test handler routes to _get_issue."""
|
|
mock_client.get.return_value = sample_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"index": 42,
|
|
}
|
|
|
|
result = await handle_issue_tool("gitea_get_issue", arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Issue #42" in result[0].text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_create_issue(self, mock_client, sample_issue):
|
|
"""Test handler routes to _create_issue."""
|
|
mock_client.post.return_value = sample_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"title": "New Issue",
|
|
}
|
|
|
|
result = await handle_issue_tool("gitea_create_issue", arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Created issue" in result[0].text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_update_issue(self, mock_client, sample_issue):
|
|
"""Test handler routes to _update_issue."""
|
|
mock_client.patch.return_value = sample_issue
|
|
|
|
arguments = {
|
|
"owner": "testowner",
|
|
"repo": "testrepo",
|
|
"index": 42,
|
|
"title": "Updated",
|
|
}
|
|
|
|
result = await handle_issue_tool("gitea_update_issue", arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Updated issue" in result[0].text
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_handle_unknown_tool(self, mock_client):
|
|
"""Test handler with unknown tool name."""
|
|
result = await handle_issue_tool("gitea_unknown_tool", {}, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Unknown issue 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_issue_tool("gitea_list_issues", arguments, mock_client)
|
|
|
|
assert len(result) == 1
|
|
assert "Error:" in result[0].text
|
|
assert "API Error" in result[0].text
|