generated from personal-projects/leo-claude-mktplace
Sprint 01 Release: Gitea MCP Server v1.0.0 #8
@@ -33,6 +33,7 @@ dependencies = [
|
||||
dev = [
|
||||
"pytest>=7.0.0",
|
||||
"pytest-asyncio>=0.21.0",
|
||||
"pytest-cov>=4.0.0",
|
||||
]
|
||||
|
||||
[project.scripts]
|
||||
|
||||
21
run_tests.sh
Executable file
21
run_tests.sh
Executable file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
# Test runner script for gitea-mcp-remote
|
||||
|
||||
set -e
|
||||
|
||||
echo "Running gitea-mcp-remote test suite..."
|
||||
echo
|
||||
|
||||
# Install dev dependencies if not already installed
|
||||
if ! command -v pytest &> /dev/null; then
|
||||
echo "Installing dev dependencies..."
|
||||
pip install -e ".[dev]"
|
||||
echo
|
||||
fi
|
||||
|
||||
# Run tests with coverage
|
||||
echo "Running tests with coverage..."
|
||||
python -m pytest tests/ -v --cov=src/gitea_mcp --cov-report=term-missing --cov-report=html
|
||||
|
||||
echo
|
||||
echo "Coverage report saved to htmlcov/index.html"
|
||||
103
tests/conftest.py
Normal file
103
tests/conftest.py
Normal file
@@ -0,0 +1,103 @@
|
||||
"""Shared pytest fixtures for Gitea MCP tests."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock
|
||||
from gitea_mcp.auth import AuthConfig
|
||||
from gitea_mcp.client import GiteaClient
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_config(monkeypatch):
|
||||
"""Create mock authentication config.
|
||||
|
||||
This fixture sets up test environment variables and returns
|
||||
a configured AuthConfig instance for testing.
|
||||
"""
|
||||
monkeypatch.setenv("GITEA_API_URL", "http://gitea.example.com/api/v1")
|
||||
monkeypatch.setenv("GITEA_API_TOKEN", "test_token_123")
|
||||
return AuthConfig()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_client(mock_config):
|
||||
"""Create a mock GiteaClient instance.
|
||||
|
||||
Returns a GiteaClient with mocked HTTP methods that don't make
|
||||
real API calls. Use this for testing tool handlers.
|
||||
"""
|
||||
client = GiteaClient(mock_config, timeout=10.0)
|
||||
|
||||
# Mock the internal HTTP client methods
|
||||
client.get = AsyncMock()
|
||||
client.post = AsyncMock()
|
||||
client.patch = AsyncMock()
|
||||
client.delete = AsyncMock()
|
||||
|
||||
# Mock context manager
|
||||
client.__aenter__ = AsyncMock(return_value=client)
|
||||
client.__aexit__ = AsyncMock(return_value=None)
|
||||
|
||||
return client
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_issue():
|
||||
"""Sample issue data for testing.
|
||||
|
||||
Returns a dict representing a typical Gitea issue response.
|
||||
"""
|
||||
return {
|
||||
"id": 1,
|
||||
"number": 42,
|
||||
"title": "Test Issue",
|
||||
"body": "This is a test issue",
|
||||
"state": "open",
|
||||
"created_at": "2024-01-15T10:00:00Z",
|
||||
"updated_at": "2024-01-15T12:00:00Z",
|
||||
"html_url": "http://gitea.example.com/test/repo/issues/42",
|
||||
"labels": [
|
||||
{"id": 1, "name": "bug", "color": "ff0000"},
|
||||
{"id": 2, "name": "priority-high", "color": "ff9900"},
|
||||
],
|
||||
"milestone": {
|
||||
"id": 10,
|
||||
"title": "v1.0",
|
||||
"state": "open",
|
||||
},
|
||||
"assignees": [
|
||||
{"id": 100, "login": "testuser"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_label():
|
||||
"""Sample label data for testing.
|
||||
|
||||
Returns a dict representing a typical Gitea label response.
|
||||
"""
|
||||
return {
|
||||
"id": 1,
|
||||
"name": "bug",
|
||||
"color": "ff0000",
|
||||
"description": "Something isn't working",
|
||||
}
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def sample_milestone():
|
||||
"""Sample milestone data for testing.
|
||||
|
||||
Returns a dict representing a typical Gitea milestone response.
|
||||
"""
|
||||
return {
|
||||
"id": 10,
|
||||
"title": "v1.0",
|
||||
"description": "First major release",
|
||||
"state": "open",
|
||||
"open_issues": 5,
|
||||
"closed_issues": 15,
|
||||
"created_at": "2024-01-01T00:00:00Z",
|
||||
"updated_at": "2024-01-15T12:00:00Z",
|
||||
"due_on": "2024-12-31T23:59:59Z",
|
||||
}
|
||||
372
tests/test_issues.py
Normal file
372
tests/test_issues.py
Normal file
@@ -0,0 +1,372 @@
|
||||
"""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
|
||||
256
tests/test_labels.py
Normal file
256
tests/test_labels.py
Normal file
@@ -0,0 +1,256 @@
|
||||
"""Tests for label operations tools."""
|
||||
|
||||
import pytest
|
||||
from mcp.types import Tool, TextContent
|
||||
from gitea_mcp.tools.labels import (
|
||||
get_label_tools,
|
||||
handle_label_tool,
|
||||
_list_labels,
|
||||
_create_label,
|
||||
)
|
||||
from gitea_mcp.client import GiteaClientError
|
||||
|
||||
|
||||
class TestLabelToolDefinitions:
|
||||
"""Test label tool schema definitions."""
|
||||
|
||||
def test_get_label_tools_returns_list(self):
|
||||
"""Test that get_label_tools returns a list of Tool objects."""
|
||||
tools = get_label_tools()
|
||||
assert isinstance(tools, list)
|
||||
assert len(tools) == 2
|
||||
assert all(isinstance(tool, Tool) for tool in tools)
|
||||
|
||||
def test_list_labels_tool_schema(self):
|
||||
"""Test gitea_list_labels tool has correct schema."""
|
||||
tools = get_label_tools()
|
||||
list_tool = next(t for t in tools if t.name == "gitea_list_labels")
|
||||
|
||||
assert list_tool.name == "gitea_list_labels"
|
||||
assert "list" in list_tool.description.lower()
|
||||
assert "label" 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"]
|
||||
|
||||
def test_create_label_tool_schema(self):
|
||||
"""Test gitea_create_label tool has correct schema."""
|
||||
tools = get_label_tools()
|
||||
create_tool = next(t for t in tools if t.name == "gitea_create_label")
|
||||
|
||||
assert create_tool.name == "gitea_create_label"
|
||||
assert "create" in create_tool.description.lower()
|
||||
|
||||
schema = create_tool.inputSchema
|
||||
assert set(schema["required"]) == {"owner", "repo", "name", "color"}
|
||||
assert "name" in schema["properties"]
|
||||
assert "color" in schema["properties"]
|
||||
assert "description" in schema["properties"]
|
||||
assert "hex" in schema["properties"]["color"]["description"].lower()
|
||||
|
||||
|
||||
class TestListLabels:
|
||||
"""Test _list_labels function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_labels_success(self, mock_client, sample_label):
|
||||
"""Test successful label listing."""
|
||||
additional_label = {
|
||||
"id": 2,
|
||||
"name": "enhancement",
|
||||
"color": "00ff00",
|
||||
"description": "New feature or request",
|
||||
}
|
||||
mock_client.get.return_value = [sample_label, additional_label]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await _list_labels(mock_client, arguments)
|
||||
|
||||
assert isinstance(result, list)
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert "bug" in result[0].text
|
||||
assert "enhancement" in result[0].text
|
||||
assert "#ff0000" in result[0].text
|
||||
assert "#00ff00" in result[0].text
|
||||
assert "Something isn't working" in result[0].text
|
||||
assert "New feature" in result[0].text
|
||||
|
||||
mock_client.get.assert_called_once_with("/repos/testowner/testrepo/labels")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_labels_empty(self, mock_client):
|
||||
"""Test listing when no labels found."""
|
||||
mock_client.get.return_value = []
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await _list_labels(mock_client, arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "No labels found" in result[0].text
|
||||
assert "testowner/testrepo" in result[0].text
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_labels_without_description(self, mock_client):
|
||||
"""Test listing labels without descriptions."""
|
||||
label_no_desc = {
|
||||
"id": 1,
|
||||
"name": "bug",
|
||||
"color": "ff0000",
|
||||
}
|
||||
mock_client.get.return_value = [label_no_desc]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await _list_labels(mock_client, arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "bug" in result[0].text
|
||||
assert "#ff0000" in result[0].text
|
||||
|
||||
|
||||
class TestCreateLabel:
|
||||
"""Test _create_label function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_label_minimal(self, mock_client, sample_label):
|
||||
"""Test creating label with minimal required fields."""
|
||||
mock_client.post.return_value = sample_label
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"name": "bug",
|
||||
"color": "ff0000",
|
||||
}
|
||||
|
||||
result = await _create_label(mock_client, arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Created label 'bug'" in result[0].text
|
||||
assert "#ff0000" in result[0].text
|
||||
assert "testowner/testrepo" in result[0].text
|
||||
|
||||
mock_client.post.assert_called_once()
|
||||
call_args = mock_client.post.call_args
|
||||
assert call_args[0][0] == "/repos/testowner/testrepo/labels"
|
||||
payload = call_args[0][1]
|
||||
assert payload["name"] == "bug"
|
||||
assert payload["color"] == "ff0000"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_label_with_description(self, mock_client, sample_label):
|
||||
"""Test creating label with description."""
|
||||
mock_client.post.return_value = sample_label
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"name": "bug",
|
||||
"color": "ff0000",
|
||||
"description": "Something isn't working",
|
||||
}
|
||||
|
||||
result = await _create_label(mock_client, arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
|
||||
call_args = mock_client.post.call_args
|
||||
payload = call_args[0][1]
|
||||
assert payload["description"] == "Something isn't working"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_create_label_strips_hash(self, mock_client, sample_label):
|
||||
"""Test creating label with # prefix is handled correctly."""
|
||||
mock_client.post.return_value = sample_label
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"name": "bug",
|
||||
"color": "#ff0000",
|
||||
}
|
||||
|
||||
result = await _create_label(mock_client, arguments)
|
||||
|
||||
call_args = mock_client.post.call_args
|
||||
payload = call_args[0][1]
|
||||
assert payload["color"] == "ff0000"
|
||||
assert not payload["color"].startswith("#")
|
||||
|
||||
|
||||
class TestLabelToolHandler:
|
||||
"""Test handle_label_tool function."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_list_labels(self, mock_client, sample_label):
|
||||
"""Test handler routes to _list_labels."""
|
||||
mock_client.get.return_value = [sample_label]
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
}
|
||||
|
||||
result = await handle_label_tool(mock_client, "gitea_list_labels", arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
assert isinstance(result[0], TextContent)
|
||||
assert "bug" in result[0].text
|
||||
mock_client.get.assert_called_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_create_label(self, mock_client, sample_label):
|
||||
"""Test handler routes to _create_label."""
|
||||
mock_client.post.return_value = sample_label
|
||||
|
||||
arguments = {
|
||||
"owner": "testowner",
|
||||
"repo": "testrepo",
|
||||
"name": "bug",
|
||||
"color": "ff0000",
|
||||
}
|
||||
|
||||
result = await handle_label_tool(mock_client, "gitea_create_label", arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Created label" 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_label_tool(mock_client, "gitea_unknown_tool", {})
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Unknown label 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_label_tool(mock_client, "gitea_list_labels", arguments)
|
||||
|
||||
assert len(result) == 1
|
||||
assert "Gitea API error:" in result[0].text
|
||||
assert "API Error" in result[0].text
|
||||
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