Skip to main content
Glama
test_tools.py17.4 kB
"""Tests for MCP tools implementation.""" from unittest.mock import AsyncMock import pytest from clickup_mcp.client import ClickUpAPIError from clickup_mcp.tools import ClickUpTools class TestClickUpTools: """Test MCP tools functionality.""" @pytest.fixture async def tools(self, mock_client): """Create tools instance with mocked client.""" return ClickUpTools(mock_client) @pytest.mark.asyncio async def test_create_task(self, tools, sample_task): """Test create_task tool.""" from clickup_mcp.models import Task # Mock create_task to return a Task object task_obj = Task(**sample_task) tools.client.create_task = AsyncMock(return_value=task_obj) result = await tools.create_task( title="Test Task", description="Test description", list_id="list123", ) # Check the expected return format from tools.create_task assert result["id"] == "abc123" assert result["name"] == "Test Task" assert result["url"] == "https://app.clickup.com/t/abc123" assert result["created"] is True tools.client.create_task.assert_called_once() @pytest.mark.asyncio async def test_create_task_with_list_name(self, tools, sample_task, sample_list): """Test create_task with list name instead of ID.""" from clickup_mcp.models import List as ClickUpList from clickup_mcp.models import Task # Mock the client methods that will be called list_obj = ClickUpList(**sample_list) task_obj = Task(**sample_task) tools.client.find_list_by_name = AsyncMock(return_value=list_obj) tools.client.create_task = AsyncMock(return_value=task_obj) result = await tools.create_task( title="Test Task", list_name="Test List", ) # Check the expected tools.create_task return format assert result["id"] == "abc123" assert result["name"] == "Test Task" assert result["created"] is True tools.client.find_list_by_name.assert_called_once_with("Test List") tools.client.create_task.assert_called_once() @pytest.mark.asyncio async def test_get_task_with_custom_id(self, tools, sample_task): """Test getting task with custom ID format.""" from clickup_mcp.models import Task # Mock task object task_obj = Task(**sample_task) # Mock the _resolve_task_id method directly tools._resolve_task_id = AsyncMock(return_value=task_obj) result = await tools.get_task("gh-123") # Check the expected return format assert result["id"] == "abc123" assert result["name"] == "Test Task" assert result["url"] == "https://app.clickup.com/t/abc123" tools._resolve_task_id.assert_called_once_with("gh-123", False) @pytest.mark.asyncio async def test_update_task(self, tools, sample_task): """Test update_task tool.""" from clickup_mcp.models import Task # Mock task resolution and update task_obj = Task(**sample_task) updated_task_data = {**sample_task, "name": "Updated Task"} updated_task_obj = Task(**updated_task_data) tools._resolve_task_id = AsyncMock(return_value=task_obj) tools.client.update_task = AsyncMock(return_value=updated_task_obj) result = await tools.update_task( task_id="abc123", title="Updated Task", ) # Check the tools.update_task return format assert result["id"] == "abc123" assert result["name"] == "Updated Task" tools._resolve_task_id.assert_called_once_with("abc123") tools.client.update_task.assert_called_once() @pytest.mark.asyncio async def test_list_tasks_with_filters(self, tools, sample_task): """Test listing tasks with various filters.""" from clickup_mcp.models import Task # Mock the client method that will actually be called task_obj = Task(**sample_task) tools.client.get_tasks = AsyncMock(return_value=[task_obj]) result = await tools.list_tasks( list_id="list123", status="todo", assignee=123, ) # Check tools.list_tasks return format assert "tasks" in result assert len(result["tasks"]) == 1 assert result["tasks"][0]["id"] == "abc123" tools.client.get_tasks.assert_called_once() @pytest.mark.asyncio async def test_search_tasks(self, tools, sample_task): """Test searching tasks.""" from clickup_mcp.models import Task # Mock client.search_tasks to return List[Task] task_obj = Task(**sample_task) tools.client.search_tasks = AsyncMock(return_value=[task_obj]) result = await tools.search_tasks( query="bug fix", space_id="space123", ) # Check tools.search_tasks return format assert "tasks" in result assert len(result["tasks"]) == 1 assert result["tasks"][0]["id"] == "abc123" tools.client.search_tasks.assert_called_once() @pytest.mark.asyncio async def test_bulk_update_tasks(self, tools): """Test bulk updating tasks.""" from clickup_mcp.models import Task # Mock client.update_task to return Task objects task1_data = { "id": "task1", "name": "Task 1", "status": { "id": "status1", "status": "done", "color": "#ffffff", "orderindex": 0, "type": "closed", }, "creator": {"id": 123, "username": "testuser"}, "list": {"id": "list123", "name": "Test List"}, "folder": {"id": "folder123", "name": "Test Folder"}, "space": {"id": "space123", "name": "Test Space"}, "url": "https://app.clickup.com/t/task1", "assignees": [], "tags": [], "custom_fields": [], } task2_data = { "id": "task2", "name": "Task 2", "status": { "id": "status1", "status": "done", "color": "#ffffff", "orderindex": 0, "type": "closed", }, "creator": {"id": 123, "username": "testuser"}, "list": {"id": "list123", "name": "Test List"}, "folder": {"id": "folder123", "name": "Test Folder"}, "space": {"id": "space123", "name": "Test Space"}, "url": "https://app.clickup.com/t/task2", "assignees": [], "tags": [], "custom_fields": [], } task1_obj = Task(**task1_data) task2_obj = Task(**task2_data) # Mock task resolution and update tools._resolve_task_id = AsyncMock(side_effect=[task1_obj, task2_obj]) tools.client.update_task = AsyncMock() result = await tools.bulk_update_tasks( task_ids=["task1", "task2"], updates={"status": "done"}, ) # Check actual return format from implementation assert result["updated"] == ["task1", "task2"] assert result["failed"] == [] @pytest.mark.asyncio async def test_create_task_from_template(self, tools, sample_task): """Test creating task from template.""" from clickup_mcp.models import Task task_obj = Task(**sample_task) tools.client.create_task = AsyncMock(return_value=task_obj) result = await tools.create_task_from_template( template_name="bug_report", list_id="list123", customizations={"title": "Login button not working"}, ) # Check tools.create_task_from_template return format assert result["id"] == "abc123" assert result["name"] == "Test Task" assert result["template"] == "bug_report" assert result["url"] == "https://app.clickup.com/t/abc123" tools.client.create_task.assert_called_once() @pytest.mark.asyncio async def test_create_task_chain(self, tools): """Test creating a chain of dependent tasks.""" from clickup_mcp.models import Task # Create Task objects to mock client.create_task returns task1_data = {**self.get_base_task_data(), "id": "task1", "name": "Design"} task2_data = {**self.get_base_task_data(), "id": "task2", "name": "Implementation"} task3_data = {**self.get_base_task_data(), "id": "task3", "name": "Testing"} task1 = Task(**task1_data) task2 = Task(**task2_data) task3 = Task(**task3_data) # Mock task creation to return Task objects tools.client.create_task = AsyncMock(side_effect=[task1, task2, task3]) chain_tasks = [ {"title": "Design", "description": "Design phase"}, {"title": "Implementation", "description": "Code it"}, {"title": "Testing", "description": "Test it"}, ] result = await tools.create_task_chain( list_id="list123", tasks=chain_tasks, ) assert result["created"] == 3 assert len(result["tasks"]) == 3 assert result["linked"] is True assert result["tasks"][0]["id"] == "task1" assert result["tasks"][1]["id"] == "task2" assert result["tasks"][2]["id"] == "task3" @pytest.mark.asyncio async def test_get_team_workload(self, tools, sample_user): """Test getting team workload.""" from clickup_mcp.models import Task, User # Create User objects for assignees user_obj = User(**sample_user) # Create sample tasks with assignees task1_data = {**self.get_base_task_data(), "id": "task1", "assignees": [user_obj]} task2_data = {**self.get_base_task_data(), "id": "task2", "assignees": [user_obj]} task1 = Task(**task1_data) task2 = Task(**task2_data) # Mock get_tasks which is what actually gets called tools.client.get_tasks = AsyncMock(return_value=[task1, task2]) result = await tools.get_team_workload("space123") assert result["space_id"] == "space123" assert result["total_tasks"] == 2 assert len(result["team_workload"]) == 1 assert result["team_workload"][0]["user_id"] == 123 assert result["team_workload"][0]["username"] == "testuser" assert result["team_workload"][0]["task_count"] == 2 def get_base_task_data(self): """Helper to get base task data for testing.""" return { "name": "Test Task", "description": "Test description", "status": { "id": "status123", "status": "todo", "color": "#ffffff", "orderindex": 0, "type": "open", }, "creator": { "id": 123, "username": "testuser", "email": "test@example.com", "color": "#000000", "initials": "TU", "profile_picture": None, }, "list": {"id": "list123", "name": "Test List"}, "folder": {"id": "folder123", "name": "Test Folder"}, "space": {"id": "space123", "name": "Test Space"}, "url": "https://app.clickup.com/t/abc123", "assignees": [], "tags": [], "custom_fields": [], } @pytest.mark.asyncio async def test_log_time(self, tools, sample_task): """Test logging time on a task.""" from clickup_mcp.models import Task # Mock task resolution task_obj = Task(**sample_task) tools._resolve_task_id = AsyncMock(return_value=task_obj) # Mock the _request method that is actually called tools.client._request = AsyncMock() result = await tools.log_time( task_id="abc123", duration="2h 30m", description="Working on feature", ) assert result["logged"] is True assert result["task_id"] == "abc123" assert result["duration_ms"] == 9000000 # 2.5 hours in ms assert result["duration"] == "2h 30m" tools.client._request.assert_called_once_with( "POST", "/team/test_workspace/time_entries", json={"duration": 9000000, "task_id": "abc123", "description": "Working on feature"}, ) @pytest.mark.asyncio async def test_get_task_analytics(self, tools, sample_task): """Test getting task analytics.""" from clickup_mcp.models import Task # Mock completed and in-progress tasks completed_task_data = { **sample_task, "status": { "id": "status1", "status": "done", "color": "#ffffff", "orderindex": 0, "type": "closed", }, } in_progress_task_data = { **sample_task, "status": { "id": "status2", "status": "in_progress", "color": "#ffffff", "orderindex": 0, "type": "custom", }, } # Create Task objects from the data completed_task = Task(**completed_task_data) in_progress_task = Task(**in_progress_task_data) regular_task = Task(**sample_task) # Mock search_tasks which is what actually gets called tools.client.search_tasks = AsyncMock( return_value=[completed_task, in_progress_task, regular_task] ) result = await tools.get_task_analytics("space123") assert result["space_id"] == "space123" assert result["period_days"] == 30 assert result["metrics"]["total_tasks_created"] == 3 assert result["metrics"]["completed_tasks"] == 1 # The completion rate should be 1/3 = 33.33% assert result["metrics"]["completion_rate"] == pytest.approx(33.33, 0.01) @pytest.mark.asyncio async def test_error_handling(self, tools): """Test error handling in tools.""" tools._resolve_task_id = AsyncMock(side_effect=ClickUpAPIError("Task not found", 404)) result = await tools.get_task("nonexistent") assert "error" in result assert "Task not found" in result["error"] @pytest.mark.asyncio async def test_create_task_comment(self, tools, sample_task): """Test create_task_comment tool.""" from clickup_mcp.models import Task # Mock task resolution and comment creation task_obj = Task(**sample_task) tools._resolve_task_id = AsyncMock(return_value=task_obj) comment_response = { "id": "comment123", "comment_text": "test comment 2", "user": {"username": "testuser"}, "date": "1640995200000", } tools.client.create_task_comment = AsyncMock(return_value=comment_response) result = await tools.create_task_comment(task_id="abc123", comment_text="test comment 2") # Check the expected return format assert result["task_id"] == "abc123" assert result["comment_id"] == "comment123" assert result["comment_text"] == "test comment 2" assert result["created"] is True assert result["notify_all"] is True tools.client.create_task_comment.assert_called_once_with( "abc123", "test comment 2", None, True ) @pytest.mark.asyncio async def test_create_task_comment_with_assignee(self, tools, sample_task): """Test create_task_comment with assignee.""" from clickup_mcp.models import Task # Mock task resolution and comment creation task_obj = Task(**sample_task) tools._resolve_task_id = AsyncMock(return_value=task_obj) comment_response = { "id": "comment456", "comment_text": "assigned comment", "user": {"username": "testuser"}, "date": "1640995200000", } tools.client.create_task_comment = AsyncMock(return_value=comment_response) result = await tools.create_task_comment( task_id="abc123", comment_text="assigned comment", assignee=12345, notify_all=False ) # Check the expected return format assert result["task_id"] == "abc123" assert result["comment_id"] == "comment456" assert result["comment_text"] == "assigned comment" assert result["created"] is True assert result["notify_all"] is False tools.client.create_task_comment.assert_called_once_with( "abc123", "assigned comment", 12345, False ) @pytest.mark.asyncio async def test_create_task_comment_error(self, tools): """Test create_task_comment error handling.""" # Mock task resolution failure tools._resolve_task_id = AsyncMock(side_effect=ClickUpAPIError("Task not found")) result = await tools.create_task_comment(task_id="invalid", comment_text="test comment") # Should handle error gracefully assert "error" in result assert "Task not found" in result["error"]

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/DiversioTeam/clickup-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server