"""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