| """ |
| Unit Tests for update_task MCP Tool |
| |
| Tests the update_task tool functionality including: |
| - Updates title only |
| - Updates description only |
| - Updates both title and description |
| - Error when no fields provided |
| - Error for non-existent task_id |
| - Task ownership validation |
| - Preserves unchanged fields |
| """ |
|
|
| import pytest |
|
|
| from src.tools.update_task import update_task_internal |
| from tests.utils.task_helpers import create_test_task, get_task_by_id |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_updates_title_only(mock_mcp_context, test_session): |
| """ |
| Test: update_task updates title only |
| |
| Verifies that update_task can update just the title while preserving other fields. |
| """ |
| |
| task = create_test_task( |
| test_session, |
| mock_mcp_context.user_id, |
| title="Old Title", |
| description="Original description" |
| ) |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| title="New Title" |
| ) |
|
|
| |
| assert result["status"] == "success" |
| assert result["task"]["title"] == "New Title" |
|
|
| |
| updated_task = get_task_by_id(test_session, task.id) |
| assert updated_task.title == "New Title" |
| assert updated_task.description == "Original description" |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_updates_description_only(mock_mcp_context, test_session): |
| """ |
| Test: update_task updates description only |
| |
| Verifies that update_task can update just the description while preserving title. |
| """ |
| |
| task = create_test_task( |
| test_session, |
| mock_mcp_context.user_id, |
| title="Original Title", |
| description="Old description" |
| ) |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| description="New description" |
| ) |
|
|
| |
| assert result["status"] == "success" |
| assert result["task"]["description"] == "New description" |
|
|
| |
| updated_task = get_task_by_id(test_session, task.id) |
| assert updated_task.title == "Original Title" |
| assert updated_task.description == "New description" |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_updates_both_title_and_description(mock_mcp_context, test_session): |
| """ |
| Test: update_task updates both title and description |
| |
| Verifies that update_task can update both fields simultaneously. |
| """ |
| |
| task = create_test_task( |
| test_session, |
| mock_mcp_context.user_id, |
| title="Old Title", |
| description="Old description" |
| ) |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| title="New Title", |
| description="New description" |
| ) |
|
|
| |
| assert result["status"] == "success" |
| assert result["task"]["title"] == "New Title" |
| assert result["task"]["description"] == "New description" |
|
|
| |
| updated_task = get_task_by_id(test_session, task.id) |
| assert updated_task.title == "New Title" |
| assert updated_task.description == "New description" |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_with_no_fields_returns_error(mock_mcp_context, test_session): |
| """ |
| Test: update_task with no fields returns error |
| |
| Verifies that update_task returns error when neither title nor description provided. |
| """ |
| |
| task = create_test_task(test_session, mock_mcp_context.user_id, title="Test") |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id |
| ) |
|
|
| |
| assert result["status"] == "error" |
| assert "error" in result |
| assert "field" in result["error"].lower() or "provided" in result["error"].lower() |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_with_non_existent_task_id_returns_error(mock_mcp_context): |
| """ |
| Test: update_task with non-existent task_id returns error |
| |
| Verifies that update_task returns error for non-existent task. |
| """ |
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=99999, |
| title="New Title" |
| ) |
|
|
| |
| assert result["status"] == "error" |
| assert "error" in result |
| assert "not found" in result["error"].lower() |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_validates_task_ownership(mock_mcp_context, mock_mcp_context_user2, test_session): |
| """ |
| Test: update_task validates task ownership |
| |
| Verifies that update_task returns error when trying to update another user's task. |
| """ |
| |
| task = create_test_task(test_session, mock_mcp_context_user2.user_id, title="User 2 Task") |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| title="Hacked Title" |
| ) |
|
|
| |
| assert result["status"] == "error" |
| assert "not found" in result["error"].lower() |
|
|
| |
| unchanged_task = get_task_by_id(test_session, task.id) |
| assert unchanged_task.title == "User 2 Task" |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_preserves_unchanged_fields(mock_mcp_context, test_session): |
| """ |
| Test: update_task preserves unchanged fields |
| |
| Verifies that update_task doesn't modify fields that weren't specified. |
| """ |
| |
| task = create_test_task( |
| test_session, |
| mock_mcp_context.user_id, |
| title="Original Title", |
| description="Original description", |
| completed=True |
| ) |
| original_created_at = task.created_at |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| title="New Title" |
| ) |
|
|
| |
| assert result["status"] == "success" |
|
|
| |
| updated_task = get_task_by_id(test_session, task.id) |
| assert updated_task.title == "New Title" |
| assert updated_task.description == "Original description" |
| assert updated_task.completed is True |
| assert updated_task.created_at == original_created_at |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_with_empty_string_description(mock_mcp_context, test_session): |
| """ |
| Test: update_task with empty string description |
| |
| Verifies that update_task can clear description by setting it to empty string. |
| """ |
| |
| task = create_test_task( |
| test_session, |
| mock_mcp_context.user_id, |
| title="Test", |
| description="Original description" |
| ) |
|
|
| |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| description="" |
| ) |
|
|
| |
| assert result["status"] == "success" |
|
|
| |
| updated_task = get_task_by_id(test_session, task.id) |
| assert updated_task.description == "" or updated_task.description is None |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_validates_title_length(mock_mcp_context, test_session): |
| """ |
| Test: update_task validates title length |
| |
| Verifies that update_task enforces title length constraints. |
| """ |
| |
| task = create_test_task(test_session, mock_mcp_context.user_id, title="Test") |
|
|
| |
| long_title = "A" * 201 |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| title=long_title |
| ) |
|
|
| |
| assert result["status"] == "error" |
| assert "200" in result["error"] or "exceeds" in result["error"].lower() |
|
|
|
|
| @pytest.mark.unit |
| @pytest.mark.asyncio |
| async def test_update_task_validates_description_length(mock_mcp_context, test_session): |
| """ |
| Test: update_task validates description length |
| |
| Verifies that update_task enforces description length constraints. |
| """ |
| |
| task = create_test_task(test_session, mock_mcp_context.user_id, title="Test") |
|
|
| |
| long_description = "B" * 2001 |
| result = await update_task_internal( |
| ctx=mock_mcp_context, |
| task_id=task.id, |
| description=long_description |
| ) |
|
|
| |
| assert result["status"] == "error" |
| assert "2000" in result["error"] or "exceeds" in result["error"].lower() |
|
|