File size: 3,720 Bytes
310260a
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
"""
Security Tests for mark_complete Tool

Validates security aspects of mark_complete tool:
- Task ownership enforcement
- Cross-user access prevention
"""

import pytest

from src.tools.mark_complete import mark_complete_internal
from tests.utils.task_helpers import create_test_task, get_task_by_id


@pytest.mark.security
@pytest.mark.asyncio
async def test_mark_complete_enforces_task_ownership(mock_mcp_context, mock_mcp_context_user2, test_session):
    """
    Test: mark_complete enforces task ownership

    Verifies that users can only mark their own tasks as complete.
    """
    # Setup: Create tasks for both users
    user1_task = create_test_task(test_session, mock_mcp_context.user_id, title="User 1 Task")
    user2_task = create_test_task(test_session, mock_mcp_context_user2.user_id, title="User 2 Task")

    # User 1 marks their own task complete (should succeed)
    result1 = await mark_complete_internal(
        ctx=mock_mcp_context,
        task_id=user1_task.id
    )
    assert result1["status"] == "success"

    # User 1 tries to mark user 2's task complete (should fail)
    result2 = await mark_complete_internal(
        ctx=mock_mcp_context,
        task_id=user2_task.id
    )
    assert result2["status"] == "error"
    assert "not found" in result2["error"].lower()

    # Verify user 2's task remains unchanged
    unchanged_task = get_task_by_id(test_session, user2_task.id)
    assert unchanged_task.completed is False


@pytest.mark.security
@pytest.mark.asyncio
async def test_mark_complete_with_user1_context_cannot_modify_user2_task(mock_mcp_context, mock_mcp_context_user2, test_session):
    """
    Test: mark_complete with user1 context cannot modify user2 task

    Verifies complete isolation - user 1 cannot modify user 2's tasks.
    """
    # Setup: Create task for user 2
    user2_task = create_test_task(
        test_session,
        mock_mcp_context_user2.user_id,
        title="User 2 Private Task",
        completed=False
    )

    # User 1 attempts to mark user 2's task complete
    result = await mark_complete_internal(
        ctx=mock_mcp_context,
        task_id=user2_task.id
    )

    # Assert - should fail with "not found" error (prevents information disclosure)
    assert result["status"] == "error"
    assert "not found" in result["error"].lower()

    # Verify user 2's task remains unchanged
    unchanged_task = get_task_by_id(test_session, user2_task.id)
    assert unchanged_task.completed is False
    assert unchanged_task.title == "User 2 Private Task"


@pytest.mark.security
@pytest.mark.asyncio
async def test_mark_complete_error_message_prevents_information_disclosure(mock_mcp_context, mock_mcp_context_user2, test_session):
    """
    Test: mark_complete error message prevents information disclosure

    Verifies that error messages don't reveal whether a task exists for another user.
    """
    # Setup: Create task for user 2
    user2_task = create_test_task(test_session, mock_mcp_context_user2.user_id, title="Secret Task")

    # User 1 tries to access user 2's task
    result_unauthorized = await mark_complete_internal(
        ctx=mock_mcp_context,
        task_id=user2_task.id
    )

    # User 1 tries to access non-existent task
    result_not_found = await mark_complete_internal(
        ctx=mock_mcp_context,
        task_id=99999
    )

    # Both should return same error message (prevents information disclosure)
    assert result_unauthorized["status"] == "error"
    assert result_not_found["status"] == "error"

    # Error messages should be similar (both say "not found")
    assert "not found" in result_unauthorized["error"].lower()
    assert "not found" in result_not_found["error"].lower()