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.
Files changed (68) hide show
  1. chronos_mcp/__init__.py +5 -0
  2. chronos_mcp/__main__.py +9 -0
  3. chronos_mcp/accounts.py +410 -0
  4. chronos_mcp/bulk.py +946 -0
  5. chronos_mcp/caldav_utils.py +149 -0
  6. chronos_mcp/calendars.py +204 -0
  7. chronos_mcp/config.py +187 -0
  8. chronos_mcp/credentials.py +190 -0
  9. chronos_mcp/events.py +515 -0
  10. chronos_mcp/exceptions.py +477 -0
  11. chronos_mcp/journals.py +477 -0
  12. chronos_mcp/logging_config.py +23 -0
  13. chronos_mcp/models.py +202 -0
  14. chronos_mcp/py.typed +0 -0
  15. chronos_mcp/rrule.py +259 -0
  16. chronos_mcp/search.py +315 -0
  17. chronos_mcp/server.py +121 -0
  18. chronos_mcp/tasks.py +518 -0
  19. chronos_mcp/tools/__init__.py +29 -0
  20. chronos_mcp/tools/accounts.py +151 -0
  21. chronos_mcp/tools/base.py +59 -0
  22. chronos_mcp/tools/bulk.py +557 -0
  23. chronos_mcp/tools/calendars.py +142 -0
  24. chronos_mcp/tools/events.py +698 -0
  25. chronos_mcp/tools/journals.py +310 -0
  26. chronos_mcp/tools/tasks.py +414 -0
  27. chronos_mcp/utils.py +163 -0
  28. chronos_mcp/validation.py +636 -0
  29. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
  30. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
  31. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
  32. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
  33. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
  34. iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
  35. tests/__init__.py +0 -0
  36. tests/conftest.py +91 -0
  37. tests/unit/__init__.py +0 -0
  38. tests/unit/test_accounts.py +380 -0
  39. tests/unit/test_accounts_ssrf.py +134 -0
  40. tests/unit/test_base.py +135 -0
  41. tests/unit/test_bulk.py +380 -0
  42. tests/unit/test_bulk_create.py +408 -0
  43. tests/unit/test_bulk_delete.py +341 -0
  44. tests/unit/test_bulk_resource_limits.py +74 -0
  45. tests/unit/test_caldav_utils.py +300 -0
  46. tests/unit/test_calendars.py +286 -0
  47. tests/unit/test_config.py +111 -0
  48. tests/unit/test_config_validation.py +128 -0
  49. tests/unit/test_credentials_security.py +189 -0
  50. tests/unit/test_cryptography_security.py +178 -0
  51. tests/unit/test_events.py +536 -0
  52. tests/unit/test_exceptions.py +58 -0
  53. tests/unit/test_journals.py +1097 -0
  54. tests/unit/test_models.py +95 -0
  55. tests/unit/test_race_conditions.py +202 -0
  56. tests/unit/test_recurring_events.py +156 -0
  57. tests/unit/test_rrule.py +217 -0
  58. tests/unit/test_search.py +372 -0
  59. tests/unit/test_search_advanced.py +333 -0
  60. tests/unit/test_server_input_validation.py +219 -0
  61. tests/unit/test_ssrf_protection.py +505 -0
  62. tests/unit/test_tasks.py +918 -0
  63. tests/unit/test_thread_safety.py +301 -0
  64. tests/unit/test_tools_journals.py +617 -0
  65. tests/unit/test_tools_tasks.py +968 -0
  66. tests/unit/test_url_validation_security.py +234 -0
  67. tests/unit/test_utils.py +180 -0
  68. tests/unit/test_validation.py +983 -0
@@ -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
+ )