iflow-mcp_democratize-technology-chronos-mcp 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chronos_mcp/__init__.py +5 -0
- chronos_mcp/__main__.py +9 -0
- chronos_mcp/accounts.py +410 -0
- chronos_mcp/bulk.py +946 -0
- chronos_mcp/caldav_utils.py +149 -0
- chronos_mcp/calendars.py +204 -0
- chronos_mcp/config.py +187 -0
- chronos_mcp/credentials.py +190 -0
- chronos_mcp/events.py +515 -0
- chronos_mcp/exceptions.py +477 -0
- chronos_mcp/journals.py +477 -0
- chronos_mcp/logging_config.py +23 -0
- chronos_mcp/models.py +202 -0
- chronos_mcp/py.typed +0 -0
- chronos_mcp/rrule.py +259 -0
- chronos_mcp/search.py +315 -0
- chronos_mcp/server.py +121 -0
- chronos_mcp/tasks.py +518 -0
- chronos_mcp/tools/__init__.py +29 -0
- chronos_mcp/tools/accounts.py +151 -0
- chronos_mcp/tools/base.py +59 -0
- chronos_mcp/tools/bulk.py +557 -0
- chronos_mcp/tools/calendars.py +142 -0
- chronos_mcp/tools/events.py +698 -0
- chronos_mcp/tools/journals.py +310 -0
- chronos_mcp/tools/tasks.py +414 -0
- chronos_mcp/utils.py +163 -0
- chronos_mcp/validation.py +636 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +91 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_accounts.py +380 -0
- tests/unit/test_accounts_ssrf.py +134 -0
- tests/unit/test_base.py +135 -0
- tests/unit/test_bulk.py +380 -0
- tests/unit/test_bulk_create.py +408 -0
- tests/unit/test_bulk_delete.py +341 -0
- tests/unit/test_bulk_resource_limits.py +74 -0
- tests/unit/test_caldav_utils.py +300 -0
- tests/unit/test_calendars.py +286 -0
- tests/unit/test_config.py +111 -0
- tests/unit/test_config_validation.py +128 -0
- tests/unit/test_credentials_security.py +189 -0
- tests/unit/test_cryptography_security.py +178 -0
- tests/unit/test_events.py +536 -0
- tests/unit/test_exceptions.py +58 -0
- tests/unit/test_journals.py +1097 -0
- tests/unit/test_models.py +95 -0
- tests/unit/test_race_conditions.py +202 -0
- tests/unit/test_recurring_events.py +156 -0
- tests/unit/test_rrule.py +217 -0
- tests/unit/test_search.py +372 -0
- tests/unit/test_search_advanced.py +333 -0
- tests/unit/test_server_input_validation.py +219 -0
- tests/unit/test_ssrf_protection.py +505 -0
- tests/unit/test_tasks.py +918 -0
- tests/unit/test_thread_safety.py +301 -0
- tests/unit/test_tools_journals.py +617 -0
- tests/unit/test_tools_tasks.py +968 -0
- tests/unit/test_url_validation_security.py +234 -0
- tests/unit/test_utils.py +180 -0
- tests/unit/test_validation.py +983 -0
tests/unit/test_tasks.py
ADDED
|
@@ -0,0 +1,918 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for task management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import uuid
|
|
6
|
+
from datetime import datetime, timezone, timedelta
|
|
7
|
+
from unittest.mock import MagicMock, Mock, patch
|
|
8
|
+
from typing import List
|
|
9
|
+
|
|
10
|
+
import pytest
|
|
11
|
+
import caldav
|
|
12
|
+
from icalendar import Calendar as iCalendar
|
|
13
|
+
from icalendar import Todo as iTodo
|
|
14
|
+
|
|
15
|
+
from chronos_mcp.calendars import CalendarManager
|
|
16
|
+
from chronos_mcp.tasks import TaskManager
|
|
17
|
+
from chronos_mcp.models import Task, TaskStatus
|
|
18
|
+
from chronos_mcp.exceptions import (
|
|
19
|
+
CalendarNotFoundError,
|
|
20
|
+
TaskNotFoundError,
|
|
21
|
+
EventCreationError,
|
|
22
|
+
EventDeletionError,
|
|
23
|
+
ChronosError,
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestTaskManager:
|
|
28
|
+
"""Test task management functionality"""
|
|
29
|
+
|
|
30
|
+
@pytest.fixture
|
|
31
|
+
def mock_calendar_manager(self):
|
|
32
|
+
"""Mock CalendarManager"""
|
|
33
|
+
manager = Mock(spec=CalendarManager)
|
|
34
|
+
manager.accounts = Mock()
|
|
35
|
+
manager.accounts.config = Mock()
|
|
36
|
+
manager.accounts.config.config = Mock()
|
|
37
|
+
manager.accounts.config.config.default_account = "test_account"
|
|
38
|
+
return manager
|
|
39
|
+
|
|
40
|
+
@pytest.fixture
|
|
41
|
+
def mock_calendar(self):
|
|
42
|
+
"""Mock calendar object with full CalDAV feature support"""
|
|
43
|
+
calendar = Mock()
|
|
44
|
+
calendar.save_todo = Mock()
|
|
45
|
+
calendar.save_event = Mock()
|
|
46
|
+
calendar.todos = Mock()
|
|
47
|
+
calendar.events = Mock()
|
|
48
|
+
calendar.event_by_uid = Mock()
|
|
49
|
+
return calendar
|
|
50
|
+
|
|
51
|
+
@pytest.fixture
|
|
52
|
+
def mock_calendar_basic(self):
|
|
53
|
+
"""Mock calendar object with basic CalDAV support (fallback mode)"""
|
|
54
|
+
|
|
55
|
+
# Create a mock that only has specific methods
|
|
56
|
+
class BasicCalendar:
|
|
57
|
+
def __init__(self):
|
|
58
|
+
self.save_event = Mock()
|
|
59
|
+
self.events = Mock()
|
|
60
|
+
# Explicitly no save_todo, todos, or event_by_uid methods
|
|
61
|
+
|
|
62
|
+
return BasicCalendar()
|
|
63
|
+
|
|
64
|
+
@pytest.fixture
|
|
65
|
+
def sample_task_data(self):
|
|
66
|
+
"""Sample task data for testing"""
|
|
67
|
+
return {
|
|
68
|
+
"calendar_uid": "cal-123",
|
|
69
|
+
"summary": "Test Task",
|
|
70
|
+
"description": "Test task description",
|
|
71
|
+
"due": datetime(2025, 12, 31, 23, 59, tzinfo=timezone.utc),
|
|
72
|
+
"priority": 5,
|
|
73
|
+
"status": TaskStatus.NEEDS_ACTION,
|
|
74
|
+
"related_to": ["related-task-1", "related-task-2"],
|
|
75
|
+
"account_alias": "test_account",
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
@pytest.fixture
|
|
79
|
+
def sample_vtodo_ical(self):
|
|
80
|
+
"""Sample VTODO iCalendar data"""
|
|
81
|
+
cal = iCalendar()
|
|
82
|
+
task = iTodo()
|
|
83
|
+
task.add("uid", "test-task-123")
|
|
84
|
+
task.add("summary", "Test Task")
|
|
85
|
+
task.add("description", "Test task description")
|
|
86
|
+
task.add("dtstamp", datetime.now(timezone.utc))
|
|
87
|
+
task.add("due", datetime(2025, 12, 31, 23, 59, tzinfo=timezone.utc))
|
|
88
|
+
task.add("priority", 5)
|
|
89
|
+
task.add("status", "NEEDS-ACTION")
|
|
90
|
+
task.add("percent-complete", 0)
|
|
91
|
+
task.add("related-to", "related-task-1")
|
|
92
|
+
task.add("related-to", "related-task-2")
|
|
93
|
+
cal.add_component(task)
|
|
94
|
+
return cal.to_ical().decode("utf-8")
|
|
95
|
+
|
|
96
|
+
@pytest.fixture
|
|
97
|
+
def mock_caldav_task(self, sample_vtodo_ical):
|
|
98
|
+
"""Mock CalDAV task object"""
|
|
99
|
+
task = Mock()
|
|
100
|
+
task.data = sample_vtodo_ical
|
|
101
|
+
task.delete = Mock()
|
|
102
|
+
task.save = Mock()
|
|
103
|
+
return task
|
|
104
|
+
|
|
105
|
+
def test_init(self, mock_calendar_manager):
|
|
106
|
+
"""Test TaskManager initialization"""
|
|
107
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
108
|
+
assert mgr.calendars == mock_calendar_manager
|
|
109
|
+
|
|
110
|
+
def test_get_default_account_success(self, mock_calendar_manager):
|
|
111
|
+
"""Test _get_default_account returns configured default"""
|
|
112
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
113
|
+
assert mgr._get_default_account() == "test_account"
|
|
114
|
+
|
|
115
|
+
def test_get_default_account_failure(self, mock_calendar_manager):
|
|
116
|
+
"""Test _get_default_account handles exceptions gracefully"""
|
|
117
|
+
mock_calendar_manager.accounts.config.config.default_account = None
|
|
118
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
119
|
+
assert mgr._get_default_account() is None
|
|
120
|
+
|
|
121
|
+
# Phase 1: Basic CRUD Operations (25% coverage target)
|
|
122
|
+
|
|
123
|
+
@patch("chronos_mcp.tasks.uuid.uuid4")
|
|
124
|
+
def test_create_task_minimal_success(
|
|
125
|
+
self, mock_uuid, mock_calendar_manager, mock_calendar
|
|
126
|
+
):
|
|
127
|
+
"""Test create_task with minimal parameters - modern server"""
|
|
128
|
+
# Setup
|
|
129
|
+
mock_uuid.return_value = Mock()
|
|
130
|
+
mock_uuid.return_value.__str__ = Mock(return_value="test-task-123")
|
|
131
|
+
|
|
132
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
133
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
134
|
+
|
|
135
|
+
mock_caldav_task = Mock()
|
|
136
|
+
mock_calendar.save_todo.return_value = mock_caldav_task
|
|
137
|
+
|
|
138
|
+
# Execute
|
|
139
|
+
result = mgr.create_task(calendar_uid="cal-123", summary="Test Task")
|
|
140
|
+
|
|
141
|
+
# Verify
|
|
142
|
+
assert result is not None
|
|
143
|
+
assert result.uid == "test-task-123"
|
|
144
|
+
assert result.summary == "Test Task"
|
|
145
|
+
assert result.status == TaskStatus.NEEDS_ACTION
|
|
146
|
+
assert result.percent_complete == 0
|
|
147
|
+
assert result.calendar_uid == "cal-123"
|
|
148
|
+
|
|
149
|
+
mock_calendar_manager.get_calendar.assert_called_once()
|
|
150
|
+
mock_calendar.save_todo.assert_called_once()
|
|
151
|
+
|
|
152
|
+
@patch("chronos_mcp.tasks.uuid.uuid4")
|
|
153
|
+
def test_create_task_full_parameters(
|
|
154
|
+
self, mock_uuid, mock_calendar_manager, mock_calendar, sample_task_data
|
|
155
|
+
):
|
|
156
|
+
"""Test create_task with all parameters"""
|
|
157
|
+
# Setup
|
|
158
|
+
mock_uuid.return_value = Mock()
|
|
159
|
+
mock_uuid.return_value.__str__ = Mock(return_value="test-task-123")
|
|
160
|
+
|
|
161
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
162
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
163
|
+
|
|
164
|
+
mock_caldav_task = Mock()
|
|
165
|
+
mock_calendar.save_todo.return_value = mock_caldav_task
|
|
166
|
+
|
|
167
|
+
# Execute
|
|
168
|
+
result = mgr.create_task(**sample_task_data)
|
|
169
|
+
|
|
170
|
+
# Verify
|
|
171
|
+
assert result is not None
|
|
172
|
+
assert result.uid == "test-task-123"
|
|
173
|
+
assert result.summary == sample_task_data["summary"]
|
|
174
|
+
assert result.description == sample_task_data["description"]
|
|
175
|
+
assert result.due == sample_task_data["due"]
|
|
176
|
+
assert result.priority == sample_task_data["priority"]
|
|
177
|
+
assert result.status == sample_task_data["status"]
|
|
178
|
+
assert result.related_to == sample_task_data["related_to"]
|
|
179
|
+
|
|
180
|
+
@patch("chronos_mcp.tasks.uuid.uuid4")
|
|
181
|
+
def test_create_task_fallback_to_save_event(
|
|
182
|
+
self, mock_uuid, mock_calendar_manager, mock_calendar
|
|
183
|
+
):
|
|
184
|
+
"""Test create_task falls back to save_event when save_todo fails"""
|
|
185
|
+
# Setup
|
|
186
|
+
mock_uuid.return_value = Mock()
|
|
187
|
+
mock_uuid.return_value.__str__ = Mock(return_value="test-task-123")
|
|
188
|
+
|
|
189
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
190
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
191
|
+
|
|
192
|
+
# Make save_todo fail
|
|
193
|
+
mock_calendar.save_todo.side_effect = Exception("save_todo failed")
|
|
194
|
+
mock_caldav_task = Mock()
|
|
195
|
+
mock_calendar.save_event.return_value = mock_caldav_task
|
|
196
|
+
|
|
197
|
+
# Execute
|
|
198
|
+
result = mgr.create_task(calendar_uid="cal-123", summary="Test Task")
|
|
199
|
+
|
|
200
|
+
# Verify
|
|
201
|
+
assert result is not None
|
|
202
|
+
mock_calendar.save_todo.assert_called_once()
|
|
203
|
+
mock_calendar.save_event.assert_called_once()
|
|
204
|
+
|
|
205
|
+
@patch("chronos_mcp.tasks.uuid.uuid4")
|
|
206
|
+
def test_create_task_basic_server(
|
|
207
|
+
self, mock_uuid, mock_calendar_manager, mock_calendar_basic
|
|
208
|
+
):
|
|
209
|
+
"""Test create_task with basic server (no save_todo support)"""
|
|
210
|
+
# Setup
|
|
211
|
+
mock_uuid.return_value = Mock()
|
|
212
|
+
mock_uuid.return_value.__str__ = Mock(return_value="test-task-123")
|
|
213
|
+
|
|
214
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
215
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar_basic
|
|
216
|
+
|
|
217
|
+
mock_caldav_task = Mock()
|
|
218
|
+
mock_calendar_basic.save_event.return_value = mock_caldav_task
|
|
219
|
+
|
|
220
|
+
# Execute
|
|
221
|
+
result = mgr.create_task(calendar_uid="cal-123", summary="Test Task")
|
|
222
|
+
|
|
223
|
+
# Verify
|
|
224
|
+
assert result is not None
|
|
225
|
+
assert result.summary == "Test Task"
|
|
226
|
+
mock_calendar_basic.save_event.assert_called_once()
|
|
227
|
+
# save_todo should not be called since it doesn't exist
|
|
228
|
+
assert not hasattr(mock_calendar_basic, "save_todo")
|
|
229
|
+
|
|
230
|
+
def test_get_task_success_event_by_uid(
|
|
231
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
232
|
+
):
|
|
233
|
+
"""Test get_task success using event_by_uid method"""
|
|
234
|
+
# Setup
|
|
235
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
236
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
237
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
238
|
+
|
|
239
|
+
# Execute
|
|
240
|
+
result = mgr.get_task(task_uid="test-task-123", calendar_uid="cal-123")
|
|
241
|
+
|
|
242
|
+
# Verify
|
|
243
|
+
assert result is not None
|
|
244
|
+
assert result.uid == "test-task-123"
|
|
245
|
+
assert result.summary == "Test Task"
|
|
246
|
+
mock_calendar.event_by_uid.assert_called_once_with("test-task-123")
|
|
247
|
+
|
|
248
|
+
def test_list_tasks_success_todos_method(
|
|
249
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
250
|
+
):
|
|
251
|
+
"""Test list_tasks success using todos() method"""
|
|
252
|
+
# Setup
|
|
253
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
254
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
255
|
+
mock_calendar.todos.return_value = [mock_caldav_task]
|
|
256
|
+
|
|
257
|
+
# Execute
|
|
258
|
+
result = mgr.list_tasks(calendar_uid="cal-123")
|
|
259
|
+
|
|
260
|
+
# Verify
|
|
261
|
+
assert len(result) == 1
|
|
262
|
+
assert result[0].uid == "test-task-123"
|
|
263
|
+
mock_calendar.todos.assert_called_once()
|
|
264
|
+
|
|
265
|
+
def test_list_tasks_with_status_filter(
|
|
266
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
267
|
+
):
|
|
268
|
+
"""Test list_tasks with status filter"""
|
|
269
|
+
# Setup
|
|
270
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
271
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
272
|
+
mock_calendar.todos.return_value = [mock_caldav_task]
|
|
273
|
+
|
|
274
|
+
# Execute
|
|
275
|
+
result = mgr.list_tasks(
|
|
276
|
+
calendar_uid="cal-123", status_filter=TaskStatus.NEEDS_ACTION
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
# Verify
|
|
280
|
+
assert len(result) == 1
|
|
281
|
+
assert result[0].status == TaskStatus.NEEDS_ACTION
|
|
282
|
+
|
|
283
|
+
def test_update_task_summary_only(
|
|
284
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
285
|
+
):
|
|
286
|
+
"""Test update_task updating only summary field"""
|
|
287
|
+
# Setup
|
|
288
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
289
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
290
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
291
|
+
|
|
292
|
+
# Execute
|
|
293
|
+
result = mgr.update_task(
|
|
294
|
+
task_uid="test-task-123", calendar_uid="cal-123", summary="Updated Summary"
|
|
295
|
+
)
|
|
296
|
+
|
|
297
|
+
# Verify
|
|
298
|
+
assert result is not None
|
|
299
|
+
mock_caldav_task.save.assert_called_once()
|
|
300
|
+
|
|
301
|
+
def test_delete_task_success_event_by_uid(
|
|
302
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
303
|
+
):
|
|
304
|
+
"""Test delete_task success using event_by_uid"""
|
|
305
|
+
# Setup
|
|
306
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
307
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
308
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
309
|
+
|
|
310
|
+
# Execute
|
|
311
|
+
result = mgr.delete_task(calendar_uid="cal-123", task_uid="test-task-123")
|
|
312
|
+
|
|
313
|
+
# Verify
|
|
314
|
+
assert result is True
|
|
315
|
+
mock_caldav_task.delete.assert_called_once()
|
|
316
|
+
|
|
317
|
+
def test_parse_caldav_task_success(self, mock_calendar_manager, mock_caldav_task):
|
|
318
|
+
"""Test _parse_caldav_task successfully parses VTODO"""
|
|
319
|
+
# Setup
|
|
320
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
321
|
+
|
|
322
|
+
# Execute
|
|
323
|
+
result = mgr._parse_caldav_task(
|
|
324
|
+
mock_caldav_task, calendar_uid="cal-123", account_alias="test_account"
|
|
325
|
+
)
|
|
326
|
+
|
|
327
|
+
# Verify
|
|
328
|
+
assert result is not None
|
|
329
|
+
assert result.uid == "test-task-123"
|
|
330
|
+
assert result.summary == "Test Task"
|
|
331
|
+
assert result.description == "Test task description"
|
|
332
|
+
assert result.priority == 5
|
|
333
|
+
assert result.status == TaskStatus.NEEDS_ACTION
|
|
334
|
+
assert result.percent_complete == 0
|
|
335
|
+
assert "related-task-1" in result.related_to
|
|
336
|
+
assert "related-task-2" in result.related_to
|
|
337
|
+
|
|
338
|
+
# Phase 2: Error Conditions (50% coverage target)
|
|
339
|
+
|
|
340
|
+
def test_create_task_calendar_not_found(self, mock_calendar_manager):
|
|
341
|
+
"""Test create_task raises CalendarNotFoundError when calendar not found"""
|
|
342
|
+
# Setup
|
|
343
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
344
|
+
mock_calendar_manager.get_calendar.return_value = None
|
|
345
|
+
|
|
346
|
+
# Execute & Verify
|
|
347
|
+
with pytest.raises(CalendarNotFoundError):
|
|
348
|
+
mgr.create_task(calendar_uid="nonexistent-cal", summary="Test Task")
|
|
349
|
+
|
|
350
|
+
def test_create_task_authorization_error(
|
|
351
|
+
self, mock_calendar_manager, mock_calendar
|
|
352
|
+
):
|
|
353
|
+
"""Test create_task handles CalDAV authorization errors"""
|
|
354
|
+
# Setup
|
|
355
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
356
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
357
|
+
mock_calendar.save_todo.side_effect = caldav.lib.error.AuthorizationError(
|
|
358
|
+
"Auth failed"
|
|
359
|
+
)
|
|
360
|
+
mock_calendar.save_event.side_effect = caldav.lib.error.AuthorizationError(
|
|
361
|
+
"Auth failed"
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
# Execute & Verify
|
|
365
|
+
with pytest.raises(EventCreationError):
|
|
366
|
+
mgr.create_task(calendar_uid="cal-123", summary="Test Task")
|
|
367
|
+
|
|
368
|
+
def test_create_task_general_error(self, mock_calendar_manager, mock_calendar):
|
|
369
|
+
"""Test create_task handles general exceptions"""
|
|
370
|
+
# Setup
|
|
371
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
372
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
373
|
+
mock_calendar.save_todo.side_effect = Exception("Unexpected error")
|
|
374
|
+
mock_calendar.save_event.side_effect = Exception("Unexpected error")
|
|
375
|
+
|
|
376
|
+
# Execute & Verify
|
|
377
|
+
with pytest.raises(EventCreationError):
|
|
378
|
+
mgr.create_task(calendar_uid="cal-123", summary="Test Task")
|
|
379
|
+
|
|
380
|
+
def test_get_task_calendar_not_found(self, mock_calendar_manager):
|
|
381
|
+
"""Test get_task raises CalendarNotFoundError when calendar not found"""
|
|
382
|
+
# Setup
|
|
383
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
384
|
+
mock_calendar_manager.get_calendar.return_value = None
|
|
385
|
+
|
|
386
|
+
# Execute & Verify
|
|
387
|
+
with pytest.raises(CalendarNotFoundError):
|
|
388
|
+
mgr.get_task(task_uid="test-task-123", calendar_uid="nonexistent-cal")
|
|
389
|
+
|
|
390
|
+
def test_get_task_not_found_event_by_uid(
|
|
391
|
+
self, mock_calendar_manager, mock_calendar
|
|
392
|
+
):
|
|
393
|
+
"""Test get_task raises TaskNotFoundError when task not found via event_by_uid"""
|
|
394
|
+
# Setup
|
|
395
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
396
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
397
|
+
mock_calendar.event_by_uid.side_effect = Exception("Task not found")
|
|
398
|
+
mock_calendar.todos.return_value = []
|
|
399
|
+
|
|
400
|
+
# Execute & Verify
|
|
401
|
+
with pytest.raises(TaskNotFoundError):
|
|
402
|
+
mgr.get_task(task_uid="nonexistent-task", calendar_uid="cal-123")
|
|
403
|
+
|
|
404
|
+
def test_get_task_not_found_fallback_search(
|
|
405
|
+
self, mock_calendar_manager, mock_calendar
|
|
406
|
+
):
|
|
407
|
+
"""Test get_task raises TaskNotFoundError when task not found via fallback search"""
|
|
408
|
+
# Setup
|
|
409
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
410
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
411
|
+
mock_calendar.event_by_uid.side_effect = Exception("Not found")
|
|
412
|
+
mock_calendar.todos.return_value = []
|
|
413
|
+
|
|
414
|
+
# Execute & Verify
|
|
415
|
+
with pytest.raises(TaskNotFoundError):
|
|
416
|
+
mgr.get_task(task_uid="nonexistent-task", calendar_uid="cal-123")
|
|
417
|
+
|
|
418
|
+
def test_list_tasks_calendar_not_found(self, mock_calendar_manager):
|
|
419
|
+
"""Test list_tasks raises CalendarNotFoundError when calendar not found"""
|
|
420
|
+
# Setup
|
|
421
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
422
|
+
mock_calendar_manager.get_calendar.return_value = None
|
|
423
|
+
|
|
424
|
+
# Execute & Verify
|
|
425
|
+
with pytest.raises(CalendarNotFoundError):
|
|
426
|
+
mgr.list_tasks(calendar_uid="nonexistent-cal")
|
|
427
|
+
|
|
428
|
+
def test_update_task_calendar_not_found(self, mock_calendar_manager):
|
|
429
|
+
"""Test update_task raises CalendarNotFoundError when calendar not found"""
|
|
430
|
+
# Setup
|
|
431
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
432
|
+
mock_calendar_manager.get_calendar.return_value = None
|
|
433
|
+
|
|
434
|
+
# Execute & Verify
|
|
435
|
+
with pytest.raises(CalendarNotFoundError):
|
|
436
|
+
mgr.update_task(
|
|
437
|
+
task_uid="test-task-123",
|
|
438
|
+
calendar_uid="nonexistent-cal",
|
|
439
|
+
summary="Updated",
|
|
440
|
+
)
|
|
441
|
+
|
|
442
|
+
def test_update_task_not_found(self, mock_calendar_manager, mock_calendar):
|
|
443
|
+
"""Test update_task raises TaskNotFoundError when task not found"""
|
|
444
|
+
# Setup
|
|
445
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
446
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
447
|
+
mock_calendar.event_by_uid.side_effect = Exception("Not found")
|
|
448
|
+
mock_calendar.todos.return_value = []
|
|
449
|
+
|
|
450
|
+
# Execute & Verify
|
|
451
|
+
with pytest.raises(TaskNotFoundError):
|
|
452
|
+
mgr.update_task(
|
|
453
|
+
task_uid="nonexistent-task", calendar_uid="cal-123", summary="Updated"
|
|
454
|
+
)
|
|
455
|
+
|
|
456
|
+
def test_delete_task_calendar_not_found(self, mock_calendar_manager):
|
|
457
|
+
"""Test delete_task raises CalendarNotFoundError when calendar not found"""
|
|
458
|
+
# Setup
|
|
459
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
460
|
+
mock_calendar_manager.get_calendar.return_value = None
|
|
461
|
+
|
|
462
|
+
# Execute & Verify
|
|
463
|
+
with pytest.raises(CalendarNotFoundError):
|
|
464
|
+
mgr.delete_task(calendar_uid="nonexistent-cal", task_uid="test-task-123")
|
|
465
|
+
|
|
466
|
+
def test_delete_task_not_found(self, mock_calendar_manager, mock_calendar):
|
|
467
|
+
"""Test delete_task raises TaskNotFoundError when task not found"""
|
|
468
|
+
# Setup
|
|
469
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
470
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
471
|
+
mock_calendar.event_by_uid.side_effect = Exception("Not found")
|
|
472
|
+
mock_calendar.todos.return_value = []
|
|
473
|
+
|
|
474
|
+
# Execute & Verify
|
|
475
|
+
with pytest.raises(TaskNotFoundError):
|
|
476
|
+
mgr.delete_task(calendar_uid="cal-123", task_uid="nonexistent-task")
|
|
477
|
+
|
|
478
|
+
def test_delete_task_general_error(
|
|
479
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
480
|
+
):
|
|
481
|
+
"""Test delete_task handles general errors during deletion"""
|
|
482
|
+
# Setup
|
|
483
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
484
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
485
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
486
|
+
mock_caldav_task.delete.side_effect = Exception("Unexpected deletion error")
|
|
487
|
+
|
|
488
|
+
# Execute & Verify - when task is found but deletion fails, raises EventDeletionError
|
|
489
|
+
# (not TaskNotFoundError, since the task was successfully found)
|
|
490
|
+
with pytest.raises(EventDeletionError):
|
|
491
|
+
mgr.delete_task(calendar_uid="cal-123", task_uid="test-task-123")
|
|
492
|
+
|
|
493
|
+
# Phase 3: Server Compatibility (70% coverage target)
|
|
494
|
+
|
|
495
|
+
def test_get_task_fallback_to_todos_search(
|
|
496
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
497
|
+
):
|
|
498
|
+
"""Test get_task falls back to searching todos when event_by_uid fails"""
|
|
499
|
+
# Setup
|
|
500
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
501
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
502
|
+
mock_calendar.event_by_uid.side_effect = Exception("Method failed")
|
|
503
|
+
mock_calendar.todos.return_value = [mock_caldav_task]
|
|
504
|
+
|
|
505
|
+
# Execute
|
|
506
|
+
result = mgr.get_task(task_uid="test-task-123", calendar_uid="cal-123")
|
|
507
|
+
|
|
508
|
+
# Verify
|
|
509
|
+
assert result is not None
|
|
510
|
+
assert result.uid == "test-task-123"
|
|
511
|
+
mock_calendar.event_by_uid.assert_called_once()
|
|
512
|
+
mock_calendar.todos.assert_called_once()
|
|
513
|
+
|
|
514
|
+
def test_get_task_fallback_to_events_search(
|
|
515
|
+
self, mock_calendar_manager, mock_calendar_basic, mock_caldav_task
|
|
516
|
+
):
|
|
517
|
+
"""Test get_task falls back to searching events when todos not available"""
|
|
518
|
+
# Setup
|
|
519
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
520
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar_basic
|
|
521
|
+
mock_calendar_basic.events.return_value = [mock_caldav_task]
|
|
522
|
+
|
|
523
|
+
# Execute
|
|
524
|
+
result = mgr.get_task(task_uid="test-task-123", calendar_uid="cal-123")
|
|
525
|
+
|
|
526
|
+
# Verify
|
|
527
|
+
assert result is not None
|
|
528
|
+
assert result.uid == "test-task-123"
|
|
529
|
+
mock_calendar_basic.events.assert_called_once()
|
|
530
|
+
|
|
531
|
+
def test_list_tasks_fallback_to_events(
|
|
532
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
533
|
+
):
|
|
534
|
+
"""Test list_tasks falls back to events when todos() fails"""
|
|
535
|
+
# Setup
|
|
536
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
537
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
538
|
+
mock_calendar.todos.side_effect = Exception("todos() failed")
|
|
539
|
+
mock_calendar.events.return_value = [mock_caldav_task]
|
|
540
|
+
|
|
541
|
+
# Execute
|
|
542
|
+
result = mgr.list_tasks(calendar_uid="cal-123")
|
|
543
|
+
|
|
544
|
+
# Verify
|
|
545
|
+
assert len(result) == 1
|
|
546
|
+
assert result[0].uid == "test-task-123"
|
|
547
|
+
mock_calendar.todos.assert_called_once()
|
|
548
|
+
mock_calendar.events.assert_called_once()
|
|
549
|
+
|
|
550
|
+
def test_list_tasks_basic_server_events_only(
|
|
551
|
+
self, mock_calendar_manager, mock_calendar_basic, mock_caldav_task
|
|
552
|
+
):
|
|
553
|
+
"""Test list_tasks on basic server using events() only"""
|
|
554
|
+
# Setup
|
|
555
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
556
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar_basic
|
|
557
|
+
mock_calendar_basic.events.return_value = [mock_caldav_task]
|
|
558
|
+
|
|
559
|
+
# Execute
|
|
560
|
+
result = mgr.list_tasks(calendar_uid="cal-123")
|
|
561
|
+
|
|
562
|
+
# Verify
|
|
563
|
+
assert len(result) == 1
|
|
564
|
+
assert result[0].uid == "test-task-123"
|
|
565
|
+
mock_calendar_basic.events.assert_called_once()
|
|
566
|
+
|
|
567
|
+
def test_update_task_fallback_search(
|
|
568
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
569
|
+
):
|
|
570
|
+
"""Test update_task falls back to searching todos when event_by_uid fails"""
|
|
571
|
+
# Setup
|
|
572
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
573
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
574
|
+
mock_calendar.event_by_uid.side_effect = Exception("Method failed")
|
|
575
|
+
mock_calendar.todos.return_value = [mock_caldav_task]
|
|
576
|
+
|
|
577
|
+
# Execute
|
|
578
|
+
result = mgr.update_task(
|
|
579
|
+
task_uid="test-task-123", calendar_uid="cal-123", summary="Updated Summary"
|
|
580
|
+
)
|
|
581
|
+
|
|
582
|
+
# Verify
|
|
583
|
+
assert result is not None
|
|
584
|
+
mock_caldav_task.save.assert_called_once()
|
|
585
|
+
|
|
586
|
+
def test_delete_task_fallback_search(
|
|
587
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
588
|
+
):
|
|
589
|
+
"""Test delete_task falls back to searching todos when event_by_uid fails"""
|
|
590
|
+
# Setup
|
|
591
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
592
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
593
|
+
mock_calendar.event_by_uid.side_effect = Exception("Method failed")
|
|
594
|
+
mock_calendar.todos.return_value = [mock_caldav_task]
|
|
595
|
+
|
|
596
|
+
# Execute
|
|
597
|
+
result = mgr.delete_task(calendar_uid="cal-123", task_uid="test-task-123")
|
|
598
|
+
|
|
599
|
+
# Verify
|
|
600
|
+
assert result is True
|
|
601
|
+
mock_caldav_task.delete.assert_called_once()
|
|
602
|
+
|
|
603
|
+
def test_delete_task_basic_server_events_search(
|
|
604
|
+
self, mock_calendar_manager, mock_calendar_basic, mock_caldav_task
|
|
605
|
+
):
|
|
606
|
+
"""Test delete_task on basic server using events() search"""
|
|
607
|
+
# Setup
|
|
608
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
609
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar_basic
|
|
610
|
+
mock_calendar_basic.events.return_value = [mock_caldav_task]
|
|
611
|
+
|
|
612
|
+
# Execute
|
|
613
|
+
result = mgr.delete_task(calendar_uid="cal-123", task_uid="test-task-123")
|
|
614
|
+
|
|
615
|
+
# Verify
|
|
616
|
+
assert result is True
|
|
617
|
+
mock_caldav_task.delete.assert_called_once()
|
|
618
|
+
|
|
619
|
+
# Phase 4: Edge Cases and Validation (80% coverage target)
|
|
620
|
+
|
|
621
|
+
def test_create_task_priority_validation(
|
|
622
|
+
self, mock_calendar_manager, mock_calendar
|
|
623
|
+
):
|
|
624
|
+
"""Test create_task validates priority range (1-9)"""
|
|
625
|
+
# Setup
|
|
626
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
627
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
628
|
+
mock_caldav_task = Mock()
|
|
629
|
+
mock_calendar.save_todo.return_value = mock_caldav_task
|
|
630
|
+
|
|
631
|
+
# Test invalid priority (outside 1-9 range)
|
|
632
|
+
with patch("chronos_mcp.tasks.uuid.uuid4") as mock_uuid:
|
|
633
|
+
mock_uuid.return_value.__str__ = Mock(return_value="test-task-123")
|
|
634
|
+
|
|
635
|
+
result = mgr.create_task(
|
|
636
|
+
calendar_uid="cal-123",
|
|
637
|
+
summary="Test Task",
|
|
638
|
+
priority=10, # Invalid priority
|
|
639
|
+
)
|
|
640
|
+
|
|
641
|
+
# Priority should be ignored for invalid values
|
|
642
|
+
assert result is not None
|
|
643
|
+
|
|
644
|
+
def test_update_task_all_fields(
|
|
645
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
646
|
+
):
|
|
647
|
+
"""Test update_task updating all possible fields"""
|
|
648
|
+
# Setup
|
|
649
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
650
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
651
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
652
|
+
|
|
653
|
+
due_date = datetime(2026, 1, 1, 12, 0, tzinfo=timezone.utc)
|
|
654
|
+
|
|
655
|
+
# Execute
|
|
656
|
+
result = mgr.update_task(
|
|
657
|
+
task_uid="test-task-123",
|
|
658
|
+
calendar_uid="cal-123",
|
|
659
|
+
summary="Updated Summary",
|
|
660
|
+
description="Updated Description",
|
|
661
|
+
due=due_date,
|
|
662
|
+
priority=3,
|
|
663
|
+
status=TaskStatus.IN_PROCESS,
|
|
664
|
+
percent_complete=50,
|
|
665
|
+
related_to=["new-related-1", "new-related-2"],
|
|
666
|
+
)
|
|
667
|
+
|
|
668
|
+
# Verify
|
|
669
|
+
assert result is not None
|
|
670
|
+
mock_caldav_task.save.assert_called_once()
|
|
671
|
+
|
|
672
|
+
def test_update_task_clear_optional_fields(
|
|
673
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
674
|
+
):
|
|
675
|
+
"""Test update_task can clear optional fields by setting to None"""
|
|
676
|
+
# Setup
|
|
677
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
678
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
679
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
680
|
+
|
|
681
|
+
# Execute - clear description, due, priority, related_to
|
|
682
|
+
result = mgr.update_task(
|
|
683
|
+
task_uid="test-task-123",
|
|
684
|
+
calendar_uid="cal-123",
|
|
685
|
+
description="", # Clear description
|
|
686
|
+
due=None, # Clear due date
|
|
687
|
+
priority=None, # Clear priority
|
|
688
|
+
related_to=[], # Clear related tasks
|
|
689
|
+
)
|
|
690
|
+
|
|
691
|
+
# Verify
|
|
692
|
+
assert result is not None
|
|
693
|
+
mock_caldav_task.save.assert_called_once()
|
|
694
|
+
|
|
695
|
+
def test_update_task_invalid_priority_range(
|
|
696
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
697
|
+
):
|
|
698
|
+
"""Test update_task handles invalid priority values"""
|
|
699
|
+
# Setup
|
|
700
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
701
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
702
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
703
|
+
|
|
704
|
+
# Execute with invalid priority
|
|
705
|
+
result = mgr.update_task(
|
|
706
|
+
task_uid="test-task-123",
|
|
707
|
+
calendar_uid="cal-123",
|
|
708
|
+
priority=15, # Invalid priority (>9)
|
|
709
|
+
)
|
|
710
|
+
|
|
711
|
+
# Verify - should still succeed but ignore invalid priority
|
|
712
|
+
assert result is not None
|
|
713
|
+
mock_caldav_task.save.assert_called_once()
|
|
714
|
+
|
|
715
|
+
def test_update_task_percent_complete_validation(
|
|
716
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
717
|
+
):
|
|
718
|
+
"""Test update_task validates percent_complete range (0-100)"""
|
|
719
|
+
# Setup
|
|
720
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
721
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
722
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
723
|
+
|
|
724
|
+
# Execute with valid percent_complete
|
|
725
|
+
result = mgr.update_task(
|
|
726
|
+
task_uid="test-task-123", calendar_uid="cal-123", percent_complete=75
|
|
727
|
+
)
|
|
728
|
+
|
|
729
|
+
# Verify
|
|
730
|
+
assert result is not None
|
|
731
|
+
mock_caldav_task.save.assert_called_once()
|
|
732
|
+
|
|
733
|
+
def test_update_task_parsing_error(
|
|
734
|
+
self, mock_calendar_manager, mock_calendar, mock_caldav_task
|
|
735
|
+
):
|
|
736
|
+
"""Test update_task handles parsing errors gracefully"""
|
|
737
|
+
# Setup
|
|
738
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
739
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
740
|
+
mock_calendar.event_by_uid.return_value = mock_caldav_task
|
|
741
|
+
|
|
742
|
+
# Make iCalendar parsing fail
|
|
743
|
+
mock_caldav_task.data = "invalid ical data"
|
|
744
|
+
|
|
745
|
+
# Execute & Verify
|
|
746
|
+
with pytest.raises(EventCreationError):
|
|
747
|
+
mgr.update_task(
|
|
748
|
+
task_uid="test-task-123", calendar_uid="cal-123", summary="Updated"
|
|
749
|
+
)
|
|
750
|
+
|
|
751
|
+
def test_parse_caldav_task_malformed_data(self, mock_calendar_manager):
|
|
752
|
+
"""Test _parse_caldav_task handles malformed iCalendar data"""
|
|
753
|
+
# Setup
|
|
754
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
755
|
+
mock_caldav_event = Mock()
|
|
756
|
+
mock_caldav_event.data = "invalid ical data"
|
|
757
|
+
|
|
758
|
+
# Execute
|
|
759
|
+
result = mgr._parse_caldav_task(
|
|
760
|
+
mock_caldav_event, calendar_uid="cal-123", account_alias="test_account"
|
|
761
|
+
)
|
|
762
|
+
|
|
763
|
+
# Verify - should return None for malformed data
|
|
764
|
+
assert result is None
|
|
765
|
+
|
|
766
|
+
def test_parse_caldav_task_no_vtodo_component(self, mock_calendar_manager):
|
|
767
|
+
"""Test _parse_caldav_task handles iCalendar without VTODO component"""
|
|
768
|
+
# Setup
|
|
769
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
770
|
+
|
|
771
|
+
# Create iCalendar with VEVENT instead of VTODO
|
|
772
|
+
cal = iCalendar()
|
|
773
|
+
from icalendar import Event as iEvent
|
|
774
|
+
|
|
775
|
+
event = iEvent()
|
|
776
|
+
event.add("uid", "test-event-123")
|
|
777
|
+
event.add("summary", "Test Event")
|
|
778
|
+
cal.add_component(event)
|
|
779
|
+
|
|
780
|
+
mock_caldav_event = Mock()
|
|
781
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
782
|
+
|
|
783
|
+
# Execute
|
|
784
|
+
result = mgr._parse_caldav_task(
|
|
785
|
+
mock_caldav_event, calendar_uid="cal-123", account_alias="test_account"
|
|
786
|
+
)
|
|
787
|
+
|
|
788
|
+
# Verify - should return None since no VTODO component
|
|
789
|
+
assert result is None
|
|
790
|
+
|
|
791
|
+
def test_parse_caldav_task_missing_optional_fields(self, mock_calendar_manager):
|
|
792
|
+
"""Test _parse_caldav_task handles missing optional fields gracefully"""
|
|
793
|
+
# Setup
|
|
794
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
795
|
+
|
|
796
|
+
# Create minimal VTODO with only required fields
|
|
797
|
+
cal = iCalendar()
|
|
798
|
+
task = iTodo()
|
|
799
|
+
task.add("uid", "minimal-task-123")
|
|
800
|
+
task.add("summary", "Minimal Task")
|
|
801
|
+
task.add("dtstamp", datetime.now(timezone.utc))
|
|
802
|
+
# No description, due, priority, etc.
|
|
803
|
+
cal.add_component(task)
|
|
804
|
+
|
|
805
|
+
mock_caldav_event = Mock()
|
|
806
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
807
|
+
|
|
808
|
+
# Execute
|
|
809
|
+
result = mgr._parse_caldav_task(
|
|
810
|
+
mock_caldav_event, calendar_uid="cal-123", account_alias="test_account"
|
|
811
|
+
)
|
|
812
|
+
|
|
813
|
+
# Verify - should handle missing fields gracefully
|
|
814
|
+
assert result is not None
|
|
815
|
+
assert result.uid == "minimal-task-123"
|
|
816
|
+
assert result.summary == "Minimal Task"
|
|
817
|
+
assert result.description is None
|
|
818
|
+
assert result.due is None
|
|
819
|
+
assert result.priority is None
|
|
820
|
+
assert result.percent_complete == 0
|
|
821
|
+
assert result.status == TaskStatus.NEEDS_ACTION
|
|
822
|
+
assert result.related_to == []
|
|
823
|
+
|
|
824
|
+
def test_parse_caldav_task_invalid_status_value(self, mock_calendar_manager):
|
|
825
|
+
"""Test _parse_caldav_task handles invalid status values gracefully"""
|
|
826
|
+
# Setup
|
|
827
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
828
|
+
|
|
829
|
+
# Create a valid VTODO with an invalid status value
|
|
830
|
+
cal = iCalendar()
|
|
831
|
+
task = iTodo()
|
|
832
|
+
task.add("uid", "invalid-status-task")
|
|
833
|
+
task.add("summary", "Task with Invalid Status")
|
|
834
|
+
task.add("dtstamp", datetime.now(timezone.utc))
|
|
835
|
+
task.add("priority", 5)
|
|
836
|
+
task.add("percent-complete", 25)
|
|
837
|
+
task.add(
|
|
838
|
+
"status", "UNKNOWN-STATUS"
|
|
839
|
+
) # This will be accepted by iCalendar but invalid for our enum
|
|
840
|
+
cal.add_component(task)
|
|
841
|
+
|
|
842
|
+
mock_caldav_event = Mock()
|
|
843
|
+
mock_caldav_event.data = cal.to_ical().decode("utf-8")
|
|
844
|
+
|
|
845
|
+
# Execute
|
|
846
|
+
result = mgr._parse_caldav_task(
|
|
847
|
+
mock_caldav_event, calendar_uid="cal-123", account_alias="test_account"
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
# Verify - should handle invalid status gracefully
|
|
851
|
+
assert result is not None
|
|
852
|
+
assert result.uid == "invalid-status-task"
|
|
853
|
+
assert result.priority == 5
|
|
854
|
+
assert result.percent_complete == 25
|
|
855
|
+
assert (
|
|
856
|
+
result.status == TaskStatus.NEEDS_ACTION
|
|
857
|
+
) # Should fallback to default for invalid status
|
|
858
|
+
|
|
859
|
+
def test_get_task_general_error_handling(
|
|
860
|
+
self, mock_calendar_manager, mock_calendar
|
|
861
|
+
):
|
|
862
|
+
"""Test get_task handles unexpected errors gracefully"""
|
|
863
|
+
# Setup
|
|
864
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
865
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
866
|
+
mock_calendar.event_by_uid.side_effect = RuntimeError("Unexpected error")
|
|
867
|
+
mock_calendar.todos.side_effect = RuntimeError("Unexpected error")
|
|
868
|
+
|
|
869
|
+
# Execute & Verify
|
|
870
|
+
with pytest.raises(ChronosError):
|
|
871
|
+
mgr.get_task(task_uid="test-task-123", calendar_uid="cal-123")
|
|
872
|
+
|
|
873
|
+
def test_list_tasks_handles_parsing_errors(
|
|
874
|
+
self, mock_calendar_manager, mock_calendar, sample_vtodo_ical
|
|
875
|
+
):
|
|
876
|
+
"""Test list_tasks continues when individual task parsing fails"""
|
|
877
|
+
# Setup
|
|
878
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
879
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
880
|
+
|
|
881
|
+
# Create one valid and one invalid task
|
|
882
|
+
valid_task = Mock()
|
|
883
|
+
valid_task.data = sample_vtodo_ical
|
|
884
|
+
|
|
885
|
+
invalid_task = Mock()
|
|
886
|
+
invalid_task.data = "invalid ical data"
|
|
887
|
+
|
|
888
|
+
mock_calendar.todos.return_value = [valid_task, invalid_task]
|
|
889
|
+
|
|
890
|
+
# Execute
|
|
891
|
+
result = mgr.list_tasks(calendar_uid="cal-123")
|
|
892
|
+
|
|
893
|
+
# Verify - should return only the valid task
|
|
894
|
+
assert len(result) == 1
|
|
895
|
+
assert result[0].uid == "test-task-123"
|
|
896
|
+
|
|
897
|
+
@patch("chronos_mcp.tasks.uuid.uuid4")
|
|
898
|
+
def test_create_task_with_request_id(
|
|
899
|
+
self, mock_uuid, mock_calendar_manager, mock_calendar
|
|
900
|
+
):
|
|
901
|
+
"""Test create_task respects provided request_id"""
|
|
902
|
+
# Setup
|
|
903
|
+
mock_uuid.return_value.__str__ = Mock(return_value="test-task-123")
|
|
904
|
+
mgr = TaskManager(mock_calendar_manager)
|
|
905
|
+
mock_calendar_manager.get_calendar.return_value = mock_calendar
|
|
906
|
+
mock_caldav_task = Mock()
|
|
907
|
+
mock_calendar.save_todo.return_value = mock_caldav_task
|
|
908
|
+
|
|
909
|
+
# Execute
|
|
910
|
+
result = mgr.create_task(
|
|
911
|
+
calendar_uid="cal-123", summary="Test Task", request_id="custom-request-id"
|
|
912
|
+
)
|
|
913
|
+
|
|
914
|
+
# Verify
|
|
915
|
+
assert result is not None
|
|
916
|
+
mock_calendar_manager.get_calendar.assert_called_with(
|
|
917
|
+
"cal-123", None, request_id="custom-request-id"
|
|
918
|
+
)
|