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,968 @@
1
+ """
2
+ Comprehensive unit tests for chronos_mcp/tools/tasks.py module
3
+ Tests all MCP tool functions for 100% coverage with defensive programming patterns
4
+ """
5
+
6
+ import uuid
7
+ from datetime import datetime, timezone
8
+ from unittest.mock import MagicMock, Mock, patch, AsyncMock
9
+ from typing import Dict, Any
10
+ import pytest
11
+
12
+ from chronos_mcp.tools.tasks import (
13
+ create_task,
14
+ list_tasks,
15
+ update_task,
16
+ delete_task,
17
+ register_task_tools,
18
+ _managers,
19
+ )
20
+ from chronos_mcp.models import TaskStatus
21
+ from chronos_mcp.exceptions import (
22
+ CalendarNotFoundError,
23
+ EventNotFoundError,
24
+ EventCreationError,
25
+ ValidationError,
26
+ ChronosError,
27
+ )
28
+
29
+
30
+ class TestTaskToolsComprehensive:
31
+ """Test MCP task tool functions with comprehensive coverage"""
32
+
33
+ @pytest.fixture
34
+ def mock_managers(self):
35
+ """Mock managers for dependency injection"""
36
+ task_manager = Mock()
37
+ return {"task_manager": task_manager}
38
+
39
+ @pytest.fixture
40
+ def sample_task(self):
41
+ """Sample task object for testing"""
42
+ task = Mock()
43
+ task.uid = "task-123"
44
+ task.summary = "Test Task"
45
+ task.description = "Test description"
46
+ task.due = datetime(2025, 12, 31, 23, 59, tzinfo=timezone.utc)
47
+ task.priority = 5
48
+ task.status = TaskStatus.NEEDS_ACTION
49
+ task.percent_complete = 0
50
+ task.related_to = ["related-1", "related-2"]
51
+ return task
52
+
53
+ @pytest.fixture
54
+ def setup_managers(self, mock_managers):
55
+ """Setup _managers module variable"""
56
+ original = _managers.copy()
57
+ _managers.clear()
58
+ _managers.update(mock_managers)
59
+ yield
60
+ _managers.clear()
61
+ _managers.update(original)
62
+
63
+ # CREATE_TASK TOOL TESTS
64
+
65
+ @pytest.mark.asyncio
66
+ async def test_create_task_minimal_success(self, setup_managers, sample_task):
67
+ """Test create_task with minimal required parameters"""
68
+ _managers["task_manager"].create_task.return_value = sample_task
69
+
70
+ result = await create_task.fn(
71
+ calendar_uid="cal-123",
72
+ summary="Test Task",
73
+ description=None,
74
+ due=None,
75
+ priority=None,
76
+ status="NEEDS-ACTION",
77
+ related_to=None,
78
+ account=None,
79
+ )
80
+
81
+ assert result["success"] is True
82
+ assert result["task"]["uid"] == "task-123"
83
+ assert result["task"]["summary"] == "Test Task"
84
+ assert "request_id" in result
85
+ _managers["task_manager"].create_task.assert_called_once()
86
+
87
+ @pytest.mark.asyncio
88
+ async def test_create_task_full_parameters(self, setup_managers, sample_task):
89
+ """Test create_task with all parameters provided"""
90
+ _managers["task_manager"].create_task.return_value = sample_task
91
+
92
+ result = await create_task.fn(
93
+ calendar_uid="cal-123",
94
+ summary="Full Test Task",
95
+ description="Full description",
96
+ due="2025-12-31T23:59:00Z",
97
+ priority=3,
98
+ status="IN-PROCESS",
99
+ related_to=["related-1", "related-2"],
100
+ account="test_account",
101
+ )
102
+
103
+ assert result["success"] is True
104
+ assert result["task"]["status"] == "NEEDS-ACTION" # from sample_task
105
+ _managers["task_manager"].create_task.assert_called_once()
106
+
107
+ @pytest.mark.asyncio
108
+ async def test_create_task_priority_string_conversion(
109
+ self, setup_managers, sample_task
110
+ ):
111
+ """Test create_task converts string priority to int"""
112
+ _managers["task_manager"].create_task.return_value = sample_task
113
+
114
+ result = await create_task.fn(
115
+ calendar_uid="cal-123",
116
+ summary="Test Task",
117
+ description=None,
118
+ due=None,
119
+ priority="5", # String that should convert to int
120
+ status="NEEDS-ACTION",
121
+ related_to=None,
122
+ account=None,
123
+ )
124
+
125
+ assert result["success"] is True
126
+ _managers["task_manager"].create_task.assert_called_once()
127
+
128
+ @pytest.mark.asyncio
129
+ async def test_create_task_invalid_priority_string(self, setup_managers):
130
+ """Test create_task handles invalid priority string"""
131
+ result = await create_task.fn(
132
+ calendar_uid="cal-123",
133
+ summary="Test Task",
134
+ description=None,
135
+ due=None,
136
+ priority="invalid", # Cannot convert to int
137
+ status="NEEDS-ACTION",
138
+ related_to=None,
139
+ account=None,
140
+ )
141
+
142
+ assert result["success"] is False
143
+ assert "Invalid priority value" in result["error"]
144
+ assert result["error_code"] == "VALIDATION_ERROR"
145
+ assert "request_id" in result
146
+
147
+ @pytest.mark.asyncio
148
+ async def test_create_task_priority_type_error(self, setup_managers):
149
+ """Test create_task handles TypeError in priority conversion"""
150
+ result = await create_task.fn(
151
+ calendar_uid="cal-123",
152
+ summary="Test Task",
153
+ description=None,
154
+ due=None,
155
+ priority={}, # TypeError when int({})
156
+ status="NEEDS-ACTION",
157
+ related_to=None,
158
+ account=None,
159
+ )
160
+
161
+ assert result["success"] is False
162
+ assert "Invalid priority value" in result["error"]
163
+
164
+ @pytest.mark.asyncio
165
+ async def test_create_task_summary_validation_error(self, setup_managers):
166
+ """Test create_task validation error for summary"""
167
+ with patch(
168
+ "chronos_mcp.tools.tasks.InputValidator.validate_text_field"
169
+ ) as mock_validate:
170
+ mock_validate.side_effect = ValidationError("Summary too long")
171
+
172
+ result = await create_task.fn(
173
+ calendar_uid="cal-123",
174
+ summary="x" * 1000, # Very long summary
175
+ description=None,
176
+ due=None,
177
+ priority=None,
178
+ status="NEEDS-ACTION",
179
+ related_to=None,
180
+ account=None,
181
+ )
182
+
183
+ assert result["success"] is False
184
+ assert "Summary too long" in result["error"]
185
+ assert result["error_code"] == "VALIDATION_ERROR"
186
+
187
+ @pytest.mark.asyncio
188
+ async def test_create_task_description_validation_error(self, setup_managers):
189
+ """Test create_task validation error for description"""
190
+ with patch(
191
+ "chronos_mcp.tools.tasks.InputValidator.validate_text_field"
192
+ ) as mock_validate:
193
+ # Summary passes, description fails
194
+ mock_validate.side_effect = [
195
+ "Valid Summary", # First call for summary
196
+ ValidationError("Description invalid"), # Second call for description
197
+ ]
198
+
199
+ result = await create_task.fn(
200
+ calendar_uid="cal-123",
201
+ summary="Valid Summary",
202
+ description="Invalid description",
203
+ due=None,
204
+ priority=None,
205
+ status="NEEDS-ACTION",
206
+ related_to=None,
207
+ account=None,
208
+ )
209
+
210
+ assert result["success"] is False
211
+ assert "Description invalid" in result["error"]
212
+ assert result["error_code"] == "VALIDATION_ERROR"
213
+
214
+ @pytest.mark.asyncio
215
+ async def test_create_task_invalid_priority_range(self, setup_managers):
216
+ """Test create_task validates priority range"""
217
+ result = await create_task.fn(
218
+ calendar_uid="cal-123",
219
+ summary="Test Task",
220
+ description=None,
221
+ due=None,
222
+ priority=10, # Outside 1-9 range
223
+ status="NEEDS-ACTION",
224
+ related_to=None,
225
+ account=None,
226
+ )
227
+
228
+ assert result["success"] is False
229
+ assert "Priority must be between 1 and 9" in result["error"]
230
+ assert result["error_code"] == "VALIDATION_ERROR"
231
+
232
+ @pytest.mark.asyncio
233
+ async def test_create_task_invalid_status(self, setup_managers):
234
+ """Test create_task validates status enum"""
235
+ result = await create_task.fn(
236
+ calendar_uid="cal-123",
237
+ summary="Test Task",
238
+ description=None,
239
+ due=None,
240
+ priority=None,
241
+ status="INVALID-STATUS",
242
+ related_to=None,
243
+ account=None,
244
+ )
245
+
246
+ assert result["success"] is False
247
+ assert "Invalid status" in result["error"]
248
+ assert result["error_code"] == "VALIDATION_ERROR"
249
+
250
+ @pytest.mark.asyncio
251
+ async def test_create_task_due_date_none(self, setup_managers, sample_task):
252
+ """Test create_task with due date as None in response"""
253
+ sample_task.due = None
254
+ _managers["task_manager"].create_task.return_value = sample_task
255
+
256
+ result = await create_task.fn(
257
+ calendar_uid="cal-123",
258
+ summary="Test Task",
259
+ description=None,
260
+ due=None,
261
+ priority=None,
262
+ status="NEEDS-ACTION",
263
+ related_to=None,
264
+ account=None,
265
+ )
266
+
267
+ assert result["success"] is True
268
+ assert result["task"]["due"] is None
269
+
270
+ @pytest.mark.asyncio
271
+ async def test_create_task_calendar_not_found_error(self, setup_managers):
272
+ """Test create_task handles CalendarNotFoundError"""
273
+ error = CalendarNotFoundError("Calendar not found")
274
+ _managers["task_manager"].create_task.side_effect = error
275
+
276
+ result = await create_task.fn(
277
+ calendar_uid="cal-123",
278
+ summary="Test Task",
279
+ description=None,
280
+ due=None,
281
+ priority=None,
282
+ status="NEEDS-ACTION",
283
+ related_to=None,
284
+ account=None,
285
+ )
286
+
287
+ assert result["success"] is False
288
+ assert "request_id" in result
289
+
290
+ @pytest.mark.asyncio
291
+ async def test_create_task_event_creation_error(self, setup_managers):
292
+ """Test create_task handles EventCreationError"""
293
+ error = EventCreationError("Creation failed")
294
+ _managers["task_manager"].create_task.side_effect = error
295
+
296
+ result = await create_task.fn(
297
+ calendar_uid="cal-123",
298
+ summary="Test Task",
299
+ description=None,
300
+ due=None,
301
+ priority=None,
302
+ status="NEEDS-ACTION",
303
+ related_to=None,
304
+ account=None,
305
+ )
306
+
307
+ assert result["success"] is False
308
+ assert result["error_code"] == "EventCreationError"
309
+
310
+ @pytest.mark.asyncio
311
+ async def test_create_task_chronos_error(self, setup_managers):
312
+ """Test create_task handles general ChronosError"""
313
+ error = ChronosError("General error")
314
+ _managers["task_manager"].create_task.side_effect = error
315
+
316
+ result = await create_task.fn(
317
+ calendar_uid="cal-123",
318
+ summary="Test Task",
319
+ description=None,
320
+ due=None,
321
+ priority=None,
322
+ status="NEEDS-ACTION",
323
+ related_to=None,
324
+ account=None,
325
+ )
326
+
327
+ assert result["success"] is False
328
+ assert result["error_code"] == "ChronosError"
329
+
330
+ @pytest.mark.asyncio
331
+ async def test_create_task_unexpected_exception(self, setup_managers):
332
+ """Test create_task handles unexpected exceptions"""
333
+ _managers["task_manager"].create_task.side_effect = RuntimeError(
334
+ "Unexpected error"
335
+ )
336
+
337
+ result = await create_task.fn(
338
+ calendar_uid="cal-123",
339
+ summary="Test Task",
340
+ description=None,
341
+ due=None,
342
+ priority=None,
343
+ status="NEEDS-ACTION",
344
+ related_to=None,
345
+ account=None,
346
+ )
347
+
348
+ assert result["success"] is False
349
+ assert "Failed to create task" in result["error"]
350
+ assert "request_id" in result
351
+
352
+ # LIST_TASKS TOOL TESTS
353
+
354
+ @pytest.mark.asyncio
355
+ async def test_list_tasks_success(self, setup_managers, sample_task):
356
+ """Test list_tasks successful execution"""
357
+ _managers["task_manager"].list_tasks.return_value = [sample_task]
358
+
359
+ result = await list_tasks.fn(
360
+ calendar_uid="cal-123", status_filter=None, account=None
361
+ )
362
+
363
+ assert len(result["tasks"]) == 1
364
+ assert result["total"] == 1
365
+ assert result["calendar_uid"] == "cal-123"
366
+ assert "request_id" in result
367
+
368
+ @pytest.mark.asyncio
369
+ async def test_list_tasks_with_status_filter(self, setup_managers, sample_task):
370
+ """Test list_tasks with status filter"""
371
+ _managers["task_manager"].list_tasks.return_value = [sample_task]
372
+
373
+ result = await list_tasks.fn(
374
+ calendar_uid="cal-123", status_filter="NEEDS-ACTION", account=None
375
+ )
376
+
377
+ assert len(result["tasks"]) == 1
378
+ _managers["task_manager"].list_tasks.assert_called_once_with(
379
+ calendar_uid="cal-123",
380
+ status_filter=TaskStatus.NEEDS_ACTION,
381
+ account_alias=None,
382
+ )
383
+
384
+ @pytest.mark.asyncio
385
+ async def test_list_tasks_invalid_status_filter(self, setup_managers):
386
+ """Test list_tasks with invalid status filter"""
387
+ result = await list_tasks.fn(
388
+ calendar_uid="cal-123", status_filter="INVALID-STATUS", account=None
389
+ )
390
+
391
+ assert result["success"] is False
392
+ assert "Invalid status filter" in result["error"]
393
+ assert result["error_code"] == "VALIDATION_ERROR"
394
+
395
+ @pytest.mark.asyncio
396
+ async def test_list_tasks_task_due_none(self, setup_managers):
397
+ """Test list_tasks with task having None due date"""
398
+ task = Mock()
399
+ task.uid = "task-123"
400
+ task.summary = "Test Task"
401
+ task.description = "Test description"
402
+ task.due = None # No due date
403
+ task.priority = 5
404
+ task.status = TaskStatus.NEEDS_ACTION
405
+ task.percent_complete = 0
406
+ task.related_to = []
407
+
408
+ _managers["task_manager"].list_tasks.return_value = [task]
409
+
410
+ result = await list_tasks.fn(
411
+ calendar_uid="cal-123", status_filter=None, account=None
412
+ )
413
+
414
+ assert result["tasks"][0]["due"] is None
415
+
416
+ @pytest.mark.asyncio
417
+ async def test_list_tasks_calendar_not_found_error(self, setup_managers):
418
+ """Test list_tasks handles CalendarNotFoundError"""
419
+ error = CalendarNotFoundError("Calendar not found")
420
+ _managers["task_manager"].list_tasks.side_effect = error
421
+
422
+ result = await list_tasks.fn(
423
+ calendar_uid="cal-123", status_filter=None, account=None
424
+ )
425
+
426
+ assert result["tasks"] == []
427
+ assert result["total"] == 0
428
+ assert "error" in result
429
+ assert "request_id" in result
430
+
431
+ @pytest.mark.asyncio
432
+ async def test_list_tasks_chronos_error(self, setup_managers):
433
+ """Test list_tasks handles ChronosError"""
434
+ error = ChronosError("General error")
435
+ _managers["task_manager"].list_tasks.side_effect = error
436
+
437
+ result = await list_tasks.fn(
438
+ calendar_uid="cal-123", status_filter=None, account=None
439
+ )
440
+
441
+ assert result["tasks"] == []
442
+ assert result["total"] == 0
443
+ assert result["error_code"] == "ChronosError"
444
+
445
+ @pytest.mark.asyncio
446
+ async def test_list_tasks_unexpected_exception(self, setup_managers):
447
+ """Test list_tasks handles unexpected exceptions"""
448
+ _managers["task_manager"].list_tasks.side_effect = RuntimeError(
449
+ "Unexpected error"
450
+ )
451
+
452
+ result = await list_tasks.fn(
453
+ calendar_uid="cal-123", status_filter=None, account=None
454
+ )
455
+
456
+ assert result["tasks"] == []
457
+ assert result["total"] == 0
458
+ assert "Failed to list tasks" in result["error"]
459
+
460
+ # UPDATE_TASK TOOL TESTS (uses @handle_tool_errors decorator)
461
+
462
+ @pytest.mark.asyncio
463
+ async def test_update_task_success(self, setup_managers, sample_task):
464
+ """Test update_task successful execution"""
465
+ _managers["task_manager"].update_task.return_value = sample_task
466
+
467
+ result = await update_task.fn(
468
+ calendar_uid="cal-123",
469
+ task_uid="task-123",
470
+ summary="Updated Summary",
471
+ description=None,
472
+ due=None,
473
+ priority=None,
474
+ status=None,
475
+ percent_complete=None,
476
+ account=None,
477
+ request_id=None,
478
+ )
479
+
480
+ assert result["success"] is True
481
+ assert result["task"]["uid"] == "task-123"
482
+ assert "request_id" in result
483
+
484
+ @pytest.mark.asyncio
485
+ async def test_update_task_priority_string_conversion(
486
+ self, setup_managers, sample_task
487
+ ):
488
+ """Test update_task converts string priority to int"""
489
+ _managers["task_manager"].update_task.return_value = sample_task
490
+
491
+ result = await update_task.fn(
492
+ calendar_uid="cal-123",
493
+ task_uid="task-123",
494
+ summary=None,
495
+ description=None,
496
+ due=None,
497
+ priority="7",
498
+ status=None,
499
+ percent_complete=None,
500
+ account=None,
501
+ request_id=None,
502
+ )
503
+
504
+ assert result["success"] is True
505
+
506
+ @pytest.mark.asyncio
507
+ async def test_update_task_invalid_priority_string(self, setup_managers):
508
+ """Test update_task handles invalid priority string"""
509
+ result = await update_task.fn(
510
+ calendar_uid="cal-123",
511
+ task_uid="task-123",
512
+ summary=None,
513
+ description=None,
514
+ due=None,
515
+ priority="invalid",
516
+ status=None,
517
+ percent_complete=None,
518
+ account=None,
519
+ request_id=None,
520
+ )
521
+
522
+ assert result["success"] is False
523
+ assert "Invalid priority value" in result["error"]
524
+
525
+ @pytest.mark.asyncio
526
+ async def test_update_task_percent_complete_string_conversion(
527
+ self, setup_managers, sample_task
528
+ ):
529
+ """Test update_task converts string percent_complete to int"""
530
+ _managers["task_manager"].update_task.return_value = sample_task
531
+
532
+ result = await update_task.fn(
533
+ calendar_uid="cal-123",
534
+ task_uid="task-123",
535
+ summary=None,
536
+ description=None,
537
+ due=None,
538
+ priority=None,
539
+ status=None,
540
+ percent_complete="50",
541
+ account=None,
542
+ request_id=None,
543
+ )
544
+
545
+ assert result["success"] is True
546
+
547
+ @pytest.mark.asyncio
548
+ async def test_update_task_invalid_percent_complete_string(self, setup_managers):
549
+ """Test update_task handles invalid percent_complete string"""
550
+ result = await update_task.fn(
551
+ calendar_uid="cal-123",
552
+ task_uid="task-123",
553
+ summary=None,
554
+ description=None,
555
+ due=None,
556
+ priority=None,
557
+ status=None,
558
+ percent_complete="invalid",
559
+ account=None,
560
+ request_id=None,
561
+ )
562
+
563
+ assert result["success"] is False
564
+ assert "Invalid percent_complete value" in result["error"]
565
+
566
+ @pytest.mark.asyncio
567
+ async def test_update_task_priority_range_validation(self, setup_managers):
568
+ """Test update_task validates priority range"""
569
+ result = await update_task.fn(
570
+ calendar_uid="cal-123",
571
+ task_uid="task-123",
572
+ summary=None,
573
+ description=None,
574
+ due=None,
575
+ priority=15, # Outside 1-9 range
576
+ status=None,
577
+ percent_complete=None,
578
+ account=None,
579
+ request_id=None,
580
+ )
581
+
582
+ assert result["success"] is False
583
+ assert "Priority must be between 1 and 9" in result["error"]
584
+
585
+ @pytest.mark.asyncio
586
+ async def test_update_task_invalid_status(self, setup_managers):
587
+ """Test update_task validates status enum"""
588
+ result = await update_task.fn(
589
+ calendar_uid="cal-123",
590
+ task_uid="task-123",
591
+ summary=None,
592
+ description=None,
593
+ due=None,
594
+ priority=None,
595
+ status="INVALID-STATUS",
596
+ percent_complete=None,
597
+ account=None,
598
+ request_id=None,
599
+ )
600
+
601
+ assert result["success"] is False
602
+ assert "Invalid status" in result["error"]
603
+
604
+ @pytest.mark.asyncio
605
+ async def test_update_task_percent_complete_range_validation(self, setup_managers):
606
+ """Test update_task validates percent_complete range"""
607
+ result = await update_task.fn(
608
+ calendar_uid="cal-123",
609
+ task_uid="task-123",
610
+ summary=None,
611
+ description=None,
612
+ due=None,
613
+ priority=None,
614
+ status=None,
615
+ percent_complete=150, # Outside 0-100 range
616
+ account=None,
617
+ request_id=None,
618
+ )
619
+
620
+ assert result["success"] is False
621
+ assert "Percent complete must be between 0 and 100" in result["error"]
622
+
623
+ @pytest.mark.asyncio
624
+ async def test_update_task_due_none_in_response(self, setup_managers):
625
+ """Test update_task with None due date in response"""
626
+ sample_task = Mock()
627
+ sample_task.uid = "task-123"
628
+ sample_task.summary = "Test Task"
629
+ sample_task.description = "Test description"
630
+ sample_task.due = None # No due date
631
+ sample_task.priority = 5
632
+ sample_task.status = TaskStatus.NEEDS_ACTION
633
+ sample_task.percent_complete = 0
634
+ sample_task.related_to = []
635
+
636
+ _managers["task_manager"].update_task.return_value = sample_task
637
+
638
+ result = await update_task.fn(
639
+ calendar_uid="cal-123",
640
+ task_uid="task-123",
641
+ summary="Updated",
642
+ description=None,
643
+ due=None,
644
+ priority=None,
645
+ status=None,
646
+ percent_complete=None,
647
+ account=None,
648
+ request_id=None,
649
+ )
650
+
651
+ assert result["success"] is True
652
+ assert result["task"]["due"] is None
653
+
654
+ # DELETE_TASK TOOL TESTS (uses @handle_tool_errors decorator)
655
+
656
+ @pytest.mark.asyncio
657
+ async def test_delete_task_success(self, setup_managers):
658
+ """Test delete_task successful execution"""
659
+ _managers["task_manager"].delete_task.return_value = True
660
+
661
+ result = await delete_task.fn(
662
+ calendar_uid="cal-123", task_uid="task-123", account=None, request_id=None
663
+ )
664
+
665
+ assert result["success"] is True
666
+ assert "deleted successfully" in result["message"]
667
+ assert "request_id" in result
668
+
669
+ @pytest.mark.asyncio
670
+ async def test_delete_task_with_account(self, setup_managers):
671
+ """Test delete_task with account parameter"""
672
+ _managers["task_manager"].delete_task.return_value = True
673
+
674
+ result = await delete_task.fn(
675
+ calendar_uid="cal-123",
676
+ task_uid="task-123",
677
+ account="test_account",
678
+ request_id=None,
679
+ )
680
+
681
+ _managers["task_manager"].delete_task.assert_called_once_with(
682
+ calendar_uid="cal-123",
683
+ task_uid="task-123",
684
+ account_alias="test_account",
685
+ request_id=result["request_id"],
686
+ )
687
+
688
+ # REGISTER_TASK_TOOLS TESTS
689
+
690
+ def test_register_task_tools(self, mock_managers, setup_managers):
691
+ """Test register_task_tools function"""
692
+ mock_mcp = Mock()
693
+
694
+ register_task_tools(mock_mcp, mock_managers)
695
+
696
+ # Verify managers were updated - strict equality now works with clean state from fixture
697
+ assert _managers == mock_managers
698
+
699
+ # Verify all tools were registered
700
+ assert mock_mcp.tool.call_count == 4
701
+
702
+ # Verify specific tools were registered
703
+ calls = [call[0][0] for call in mock_mcp.tool.call_args_list]
704
+ assert create_task in calls
705
+ assert list_tasks in calls
706
+ assert update_task in calls
707
+ assert delete_task in calls
708
+
709
+ # FUNCTION ATTRIBUTE TESTS
710
+
711
+ def test_function_attributes_exist(self):
712
+ """Test that .fn attributes exist for backwards compatibility"""
713
+ assert hasattr(create_task, "fn")
714
+ assert hasattr(list_tasks, "fn")
715
+ assert hasattr(update_task, "fn")
716
+ assert hasattr(delete_task, "fn")
717
+
718
+ assert create_task.fn == create_task
719
+ assert list_tasks.fn == list_tasks
720
+ assert update_task.fn == update_task
721
+ assert delete_task.fn == delete_task
722
+
723
+ # EDGE CASES AND DEFENSIVE PROGRAMMING
724
+
725
+ @pytest.mark.asyncio
726
+ async def test_create_task_zero_priority(self, setup_managers):
727
+ """Test create_task with priority 0 (invalid)"""
728
+ result = await create_task.fn(
729
+ calendar_uid="cal-123",
730
+ summary="Test Task",
731
+ description=None,
732
+ due=None,
733
+ priority=0, # Below valid range
734
+ status="NEEDS-ACTION",
735
+ related_to=None,
736
+ account=None,
737
+ )
738
+
739
+ assert result["success"] is False
740
+ assert "Priority must be between 1 and 9" in result["error"]
741
+
742
+ @pytest.mark.asyncio
743
+ async def test_update_task_negative_percent_complete(self, setup_managers):
744
+ """Test update_task with negative percent_complete"""
745
+ result = await update_task.fn(
746
+ calendar_uid="cal-123",
747
+ task_uid="task-123",
748
+ summary=None,
749
+ description=None,
750
+ due=None,
751
+ priority=None,
752
+ status=None,
753
+ percent_complete=-10, # Below valid range
754
+ account=None,
755
+ request_id=None,
756
+ )
757
+
758
+ assert result["success"] is False
759
+ assert "Percent complete must be between 0 and 100" in result["error"]
760
+
761
+ @pytest.mark.asyncio
762
+ async def test_create_task_malformed_due_date(self, setup_managers):
763
+ """Test create_task with malformed due date triggering parse_datetime error"""
764
+ with patch("chronos_mcp.tools.tasks.parse_datetime") as mock_parse:
765
+ mock_parse.side_effect = ValueError("Invalid date format")
766
+
767
+ result = await create_task.fn(
768
+ calendar_uid="cal-123",
769
+ summary="Test Task",
770
+ description=None,
771
+ due="invalid-date",
772
+ priority=None,
773
+ status="NEEDS-ACTION",
774
+ related_to=None,
775
+ account=None,
776
+ )
777
+
778
+ assert result["success"] is False
779
+ assert "Failed to create task" in result["error"]
780
+
781
+ @pytest.mark.asyncio
782
+ async def test_update_task_malformed_due_date(self, setup_managers):
783
+ """Test update_task with malformed due date triggering parse_datetime error"""
784
+ with patch("chronos_mcp.tools.tasks.parse_datetime") as mock_parse:
785
+ mock_parse.side_effect = ValueError("Invalid date format")
786
+
787
+ result = await update_task.fn(
788
+ calendar_uid="cal-123",
789
+ task_uid="task-123",
790
+ summary=None,
791
+ description=None,
792
+ due="invalid-date",
793
+ priority=None,
794
+ status=None,
795
+ percent_complete=None,
796
+ account=None,
797
+ request_id=None,
798
+ )
799
+
800
+ assert result["success"] is False
801
+
802
+ @pytest.mark.asyncio
803
+ async def test_create_task_empty_summary(self, setup_managers):
804
+ """Test create_task with empty summary"""
805
+ with patch(
806
+ "chronos_mcp.tools.tasks.InputValidator.validate_text_field"
807
+ ) as mock_validate:
808
+ mock_validate.side_effect = ValidationError("Summary is required")
809
+
810
+ result = await create_task.fn(
811
+ calendar_uid="cal-123",
812
+ summary="",
813
+ description=None,
814
+ due=None,
815
+ priority=None,
816
+ status="NEEDS-ACTION",
817
+ related_to=None,
818
+ account=None,
819
+ )
820
+
821
+ assert result["success"] is False
822
+ assert "Summary is required" in result["error"]
823
+
824
+ @pytest.mark.asyncio
825
+ async def test_list_tasks_with_account(self, setup_managers, sample_task):
826
+ """Test list_tasks with account parameter"""
827
+ _managers["task_manager"].list_tasks.return_value = [sample_task]
828
+
829
+ result = await list_tasks.fn(
830
+ calendar_uid="cal-123", status_filter=None, account="test_account"
831
+ )
832
+
833
+ _managers["task_manager"].list_tasks.assert_called_once_with(
834
+ calendar_uid="cal-123", status_filter=None, account_alias="test_account"
835
+ )
836
+ assert result["total"] == 1
837
+ assert len(result["tasks"]) == 1
838
+
839
+ @pytest.mark.asyncio
840
+ async def test_update_task_all_parameters(self, setup_managers, sample_task):
841
+ """Test update_task with all parameters"""
842
+ _managers["task_manager"].update_task.return_value = sample_task
843
+
844
+ result = await update_task.fn(
845
+ calendar_uid="cal-123",
846
+ task_uid="task-123",
847
+ summary="Updated Summary",
848
+ description="Updated description",
849
+ due="2025-12-31T23:59:00Z",
850
+ priority=3,
851
+ status="IN-PROCESS",
852
+ percent_complete=75,
853
+ account="test_account",
854
+ request_id=None,
855
+ )
856
+
857
+ assert result["success"] is True
858
+ assert result["task"]["uid"] == "task-123"
859
+
860
+ @pytest.mark.asyncio
861
+ async def test_update_task_summary_validation_error(self, setup_managers):
862
+ """Test update_task validation error for summary"""
863
+ with patch(
864
+ "chronos_mcp.tools.tasks.InputValidator.validate_text_field"
865
+ ) as mock_validate:
866
+ mock_validate.side_effect = ValidationError("Summary invalid")
867
+
868
+ result = await update_task.fn(
869
+ calendar_uid="cal-123",
870
+ task_uid="task-123",
871
+ summary="Invalid summary",
872
+ description=None,
873
+ due=None,
874
+ priority=None,
875
+ status=None,
876
+ percent_complete=None,
877
+ account=None,
878
+ request_id=None,
879
+ )
880
+
881
+ assert result["success"] is False
882
+ assert "Summary invalid" in result["error"]
883
+
884
+ @pytest.mark.asyncio
885
+ async def test_update_task_description_validation_error(self, setup_managers):
886
+ """Test update_task validation error for description"""
887
+ with patch(
888
+ "chronos_mcp.tools.tasks.InputValidator.validate_text_field"
889
+ ) as mock_validate:
890
+ mock_validate.side_effect = ValidationError("Description invalid")
891
+
892
+ result = await update_task.fn(
893
+ calendar_uid="cal-123",
894
+ task_uid="task-123",
895
+ summary=None,
896
+ description="Invalid description",
897
+ due=None,
898
+ priority=None,
899
+ status=None,
900
+ percent_complete=None,
901
+ account=None,
902
+ request_id=None,
903
+ )
904
+
905
+ assert result["success"] is False
906
+ assert "Description invalid" in result["error"]
907
+
908
+ @pytest.mark.asyncio
909
+ async def test_update_task_priority_type_error(self, setup_managers):
910
+ """Test update_task handles TypeError in priority conversion"""
911
+ result = await update_task.fn(
912
+ calendar_uid="cal-123",
913
+ task_uid="task-123",
914
+ summary=None,
915
+ description=None,
916
+ due=None,
917
+ priority={}, # TypeError when int({})
918
+ status=None,
919
+ percent_complete=None,
920
+ account=None,
921
+ request_id=None,
922
+ )
923
+
924
+ assert result["success"] is False
925
+ assert "Invalid priority value" in result["error"]
926
+
927
+ @pytest.mark.asyncio
928
+ async def test_update_task_percent_complete_type_error(self, setup_managers):
929
+ """Test update_task handles TypeError in percent_complete conversion"""
930
+ result = await update_task.fn(
931
+ calendar_uid="cal-123",
932
+ task_uid="task-123",
933
+ summary=None,
934
+ description=None,
935
+ due=None,
936
+ priority=None,
937
+ status=None,
938
+ percent_complete=[], # TypeError when int([])
939
+ account=None,
940
+ request_id=None,
941
+ )
942
+
943
+ assert result["success"] is False
944
+ assert "Invalid percent_complete value" in result["error"]
945
+
946
+ @pytest.mark.asyncio
947
+ async def test_managers_not_initialized(self):
948
+ """Test behavior when _managers is not properly initialized"""
949
+ # Clear managers to simulate uninitialized state
950
+ original = _managers.copy()
951
+ _managers.clear()
952
+
953
+ try:
954
+ result = await create_task.fn(
955
+ calendar_uid="cal-123",
956
+ summary="Test Task",
957
+ description=None,
958
+ due=None,
959
+ priority=None,
960
+ status="NEEDS-ACTION",
961
+ related_to=None,
962
+ account=None,
963
+ )
964
+ # Should get an error response, not an exception
965
+ assert result["success"] is False
966
+ assert "Failed to create task" in result["error"]
967
+ finally:
968
+ _managers.update(original)