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,536 @@
1
+ """
2
+ Unit tests for event management
3
+ """
4
+
5
+ from datetime import datetime, timedelta
6
+ from unittest.mock import MagicMock, Mock, patch
7
+
8
+ import pytest
9
+ import pytz
10
+ from icalendar import Calendar as iCalendar
11
+ from icalendar import Event as iEvent
12
+
13
+ from chronos_mcp.calendars import CalendarManager
14
+ from chronos_mcp.events import EventManager
15
+
16
+
17
+ class TestEventManager:
18
+ """Test event management functionality"""
19
+
20
+ @pytest.fixture
21
+ def mock_calendar_manager(self):
22
+ """Mock CalendarManager"""
23
+ return Mock(spec=CalendarManager)
24
+
25
+ @pytest.fixture
26
+ def mock_calendar(self):
27
+ """Mock calendar object"""
28
+ calendar = Mock()
29
+ calendar.save_event = Mock()
30
+ calendar.events = Mock()
31
+ return calendar
32
+
33
+ @pytest.fixture
34
+ def sample_event_data(self):
35
+ """Sample event data for testing"""
36
+ return {
37
+ "calendar_uid": "cal-123",
38
+ "summary": "Test Meeting",
39
+ "start": datetime(2025, 7, 10, 14, 0, tzinfo=pytz.UTC),
40
+ "end": datetime(2025, 7, 10, 15, 0, tzinfo=pytz.UTC),
41
+ "description": "Test Description",
42
+ "location": "Conference Room A",
43
+ "account_alias": "test_account",
44
+ }
45
+
46
+ def test_init(self, mock_calendar_manager):
47
+ """Test EventManager initialization"""
48
+ mgr = EventManager(mock_calendar_manager)
49
+ assert mgr.calendars == mock_calendar_manager
50
+
51
+ def test_create_event_calendar_not_found(
52
+ self, mock_calendar_manager, sample_event_data
53
+ ):
54
+ """Test creating event when calendar not found"""
55
+ mock_calendar_manager.get_calendar.return_value = None
56
+ mgr = EventManager(mock_calendar_manager)
57
+
58
+ # Should raise CalendarNotFoundError
59
+ from chronos_mcp.exceptions import CalendarNotFoundError
60
+
61
+ with pytest.raises(CalendarNotFoundError) as exc_info:
62
+ mgr.create_event(**sample_event_data)
63
+
64
+ assert "cal-123" in str(exc_info.value)
65
+ mock_calendar_manager.get_calendar.assert_called_once()
66
+
67
+ @patch("chronos_mcp.events.uuid.uuid4")
68
+ def test_create_event_success(
69
+ self, mock_uuid, mock_calendar_manager, mock_calendar, sample_event_data
70
+ ):
71
+ """Test successful event creation"""
72
+ mock_uuid.return_value = "evt-test-123"
73
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
74
+ mock_caldav_event = Mock()
75
+ mock_calendar.save_event.return_value = mock_caldav_event
76
+
77
+ mgr = EventManager(mock_calendar_manager)
78
+
79
+ result = mgr.create_event(**sample_event_data)
80
+
81
+ assert result is not None
82
+ assert result.uid == "evt-test-123"
83
+ assert result.summary == "Test Meeting"
84
+ assert result.description == "Test Description"
85
+ assert result.location == "Conference Room A"
86
+ assert result.calendar_uid == "cal-123"
87
+ assert result.account_alias == "test_account"
88
+
89
+ # Verify calendar.save_event was called with proper ical data
90
+ mock_calendar.save_event.assert_called_once()
91
+ ical_data = mock_calendar.save_event.call_args[0][0]
92
+ assert "BEGIN:VCALENDAR" in ical_data
93
+ assert "Test Meeting" in ical_data
94
+
95
+ def test_create_event_with_attendees(
96
+ self, mock_calendar_manager, mock_calendar, sample_event_data
97
+ ):
98
+ """Test creating event with attendees"""
99
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
100
+
101
+ attendees = [
102
+ {
103
+ "email": "user1@example.com",
104
+ "name": "User One",
105
+ "role": "REQ-PARTICIPANT",
106
+ },
107
+ {
108
+ "email": "user2@example.com",
109
+ "name": "User Two",
110
+ "role": "OPT-PARTICIPANT",
111
+ "rsvp": False,
112
+ },
113
+ ]
114
+ sample_event_data["attendees"] = attendees
115
+
116
+ mgr = EventManager(mock_calendar_manager)
117
+ result = mgr.create_event(**sample_event_data)
118
+
119
+ assert result is not None
120
+ assert len(result.attendees) == 2
121
+ assert result.attendees[0].email == "user1@example.com"
122
+ assert result.attendees[1].role == "OPT-PARTICIPANT"
123
+
124
+ # Check ical contains attendees
125
+ ical_data = mock_calendar.save_event.call_args[0][0]
126
+ assert "ATTENDEE" in ical_data
127
+ assert "mailto:user1@example.com" in ical_data
128
+
129
+ def test_create_event_with_alarm(
130
+ self, mock_calendar_manager, mock_calendar, sample_event_data
131
+ ):
132
+ """Test creating event with alarm"""
133
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
134
+ sample_event_data["alarm_minutes"] = 15
135
+
136
+ mgr = EventManager(mock_calendar_manager)
137
+ result = mgr.create_event(**sample_event_data)
138
+
139
+ assert result is not None
140
+ assert result.alarms is not None
141
+ assert len(result.alarms) == 1
142
+ assert result.alarms[0].trigger == "-PT15M"
143
+ assert result.alarms[0].action == "DISPLAY"
144
+
145
+ # Check ical contains alarm
146
+ ical_data = mock_calendar.save_event.call_args[0][0]
147
+ assert "BEGIN:VALARM" in ical_data
148
+ assert "TRIGGER:-PT15M" in ical_data
149
+
150
+ def test_create_event_with_recurrence(
151
+ self, mock_calendar_manager, mock_calendar, sample_event_data
152
+ ):
153
+ """Test creating recurring event"""
154
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
155
+ sample_event_data["recurrence_rule"] = "FREQ=WEEKLY;BYDAY=MO,WE,FR"
156
+
157
+ mgr = EventManager(mock_calendar_manager)
158
+ result = mgr.create_event(**sample_event_data)
159
+
160
+ assert result is not None
161
+ assert result.recurrence_rule == "FREQ=WEEKLY;BYDAY=MO,WE,FR"
162
+
163
+ # Check ical contains rrule
164
+ ical_data = mock_calendar.save_event.call_args[0][0]
165
+ assert "RRULE:FREQ=WEEKLY;BYDAY=MO,WE,FR" in ical_data
166
+
167
+ def test_create_event_all_day(self, mock_calendar_manager, mock_calendar):
168
+ """Test creating all-day event"""
169
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
170
+
171
+ mgr = EventManager(mock_calendar_manager)
172
+ result = mgr.create_event(
173
+ calendar_uid="cal-123",
174
+ summary="All Day Event",
175
+ start=datetime(2025, 7, 10, 0, 0, tzinfo=pytz.UTC),
176
+ end=datetime(2025, 7, 11, 0, 0, tzinfo=pytz.UTC),
177
+ all_day=True,
178
+ )
179
+
180
+ assert result is not None
181
+ assert result.all_day is True
182
+
183
+ def test_create_event_exception(
184
+ self, mock_calendar_manager, mock_calendar, sample_event_data
185
+ ):
186
+ """Test event creation with exception"""
187
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
188
+ mock_calendar.save_event.side_effect = Exception("CalDAV error")
189
+
190
+ mgr = EventManager(mock_calendar_manager)
191
+
192
+ # Should raise EventCreationError
193
+ from chronos_mcp.exceptions import EventCreationError
194
+
195
+ with pytest.raises(EventCreationError) as exc_info:
196
+ mgr.create_event(**sample_event_data)
197
+
198
+ assert "CalDAV error" in str(exc_info.value)
199
+
200
+ def test_get_events_range_calendar_not_found(self, mock_calendar_manager):
201
+ """Test getting events when calendar not found"""
202
+ mock_calendar_manager.get_calendar.return_value = None
203
+
204
+ mgr = EventManager(mock_calendar_manager)
205
+
206
+ # Should raise CalendarNotFoundError
207
+ from chronos_mcp.exceptions import CalendarNotFoundError
208
+
209
+ with pytest.raises(CalendarNotFoundError) as exc_info:
210
+ mgr.get_events_range(
211
+ calendar_uid="cal-123",
212
+ start_date=datetime.now(),
213
+ end_date=datetime.now() + timedelta(days=1),
214
+ )
215
+
216
+ assert "cal-123" in str(exc_info.value)
217
+
218
+ def test_get_events_range_success(self, mock_calendar_manager, mock_calendar):
219
+ """Test successful event range retrieval"""
220
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
221
+
222
+ # Create mock CalDAV events
223
+ mock_event1 = Mock()
224
+ mock_event1.data = """BEGIN:VEVENT
225
+ UID:evt-1
226
+ SUMMARY:Event 1
227
+ DTSTART:20250710T140000Z
228
+ DTEND:20250710T150000Z
229
+ END:VEVENT"""
230
+
231
+ mock_event2 = Mock()
232
+ mock_event2.data = """BEGIN:VEVENT
233
+ UID:evt-2
234
+ SUMMARY:Event 2
235
+ DTSTART:20250710T160000Z
236
+ DTEND:20250710T170000Z
237
+ DESCRIPTION:Test description
238
+ LOCATION:Room B
239
+ END:VEVENT"""
240
+
241
+ mock_calendar.date_search.return_value = [mock_event1, mock_event2]
242
+
243
+ mgr = EventManager(mock_calendar_manager)
244
+ result = mgr.get_events_range(
245
+ calendar_uid="cal-123",
246
+ start_date=datetime(2025, 7, 10, 0, 0, tzinfo=pytz.UTC),
247
+ end_date=datetime(2025, 7, 11, 0, 0, tzinfo=pytz.UTC),
248
+ )
249
+
250
+ assert len(result) == 2
251
+ assert result[0].uid == "evt-1"
252
+ assert result[0].summary == "Event 1"
253
+ assert result[1].uid == "evt-2"
254
+ assert result[1].summary == "Event 2"
255
+ assert result[1].description == "Test description"
256
+ assert result[1].location == "Room B"
257
+
258
+ def test_get_events_range_with_attendees(
259
+ self, mock_calendar_manager, mock_calendar
260
+ ):
261
+ """Test getting events with attendees"""
262
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
263
+
264
+ mock_event = Mock()
265
+ mock_event.data = """BEGIN:VEVENT
266
+ UID:evt-3
267
+ SUMMARY:Meeting
268
+ DTSTART:20250710T140000Z
269
+ DTEND:20250710T150000Z
270
+ ATTENDEE;CN=User One;ROLE=REQ-PARTICIPANT:mailto:user1@example.com
271
+ ATTENDEE;CN=User Two;ROLE=OPT-PARTICIPANT;RSVP=FALSE:mailto:user2@example.com
272
+ END:VEVENT"""
273
+
274
+ mock_calendar.date_search.return_value = [mock_event]
275
+
276
+ mgr = EventManager(mock_calendar_manager)
277
+ result = mgr.get_events_range(
278
+ calendar_uid="cal-123",
279
+ start_date=datetime(2025, 7, 10, tzinfo=pytz.UTC),
280
+ end_date=datetime(2025, 7, 11, tzinfo=pytz.UTC),
281
+ )
282
+
283
+ assert len(result) == 1
284
+ assert len(result[0].attendees) == 2
285
+ assert result[0].attendees[0].email == "user1@example.com"
286
+ assert result[0].attendees[0].name == "User One"
287
+ assert result[0].attendees[1].role == "OPT-PARTICIPANT"
288
+
289
+ def test_get_events_range_exception(self, mock_calendar_manager, mock_calendar):
290
+ """Test event retrieval with exception"""
291
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
292
+ mock_calendar.events.side_effect = Exception("CalDAV error")
293
+
294
+ mgr = EventManager(mock_calendar_manager)
295
+ result = mgr.get_events_range(
296
+ calendar_uid="cal-123",
297
+ start_date=datetime.now(),
298
+ end_date=datetime.now() + timedelta(days=1),
299
+ )
300
+
301
+ assert result == []
302
+
303
+ def test_delete_event_calendar_not_found(self, mock_calendar_manager):
304
+ """Test deleting event when calendar not found"""
305
+ from unittest.mock import ANY
306
+
307
+ from chronos_mcp.exceptions import CalendarNotFoundError
308
+
309
+ mock_calendar_manager.get_calendar.return_value = None
310
+
311
+ mgr = EventManager(mock_calendar_manager)
312
+
313
+ # Should raise CalendarNotFoundError
314
+ with pytest.raises(CalendarNotFoundError) as exc_info:
315
+ mgr.delete_event("cal-123", "evt-123")
316
+
317
+ assert "cal-123" in str(exc_info.value)
318
+ mock_calendar_manager.get_calendar.assert_called_once_with(
319
+ "cal-123", None, request_id=ANY
320
+ )
321
+
322
+ def test_create_event_with_valid_rrule(self, mock_calendar_manager, mock_calendar):
323
+ """Test creating event with valid RRULE"""
324
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
325
+ mock_calendar.save_event.return_value = Mock()
326
+
327
+ mgr = EventManager(mock_calendar_manager)
328
+
329
+ # Test with daily recurrence
330
+ event = mgr.create_event(
331
+ calendar_uid="cal-123",
332
+ summary="Daily Standup",
333
+ start=datetime.now(),
334
+ end=datetime.now() + timedelta(hours=1),
335
+ recurrence_rule="FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR",
336
+ )
337
+
338
+ assert event is not None
339
+ assert event.summary == "Daily Standup"
340
+ assert event.recurrence_rule == "FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR"
341
+
342
+ # Verify the iCalendar was created with RRULE
343
+ mock_calendar.save_event.assert_called_once()
344
+ ical_data = mock_calendar.save_event.call_args[0][0]
345
+ assert "RRULE:FREQ=DAILY;BYDAY=MO,TU,WE,TH,FR" in ical_data
346
+
347
+ def test_create_event_with_invalid_rrule(
348
+ self, mock_calendar_manager, mock_calendar
349
+ ):
350
+ """Test creating event with invalid RRULE raises error"""
351
+ from chronos_mcp.exceptions import EventCreationError
352
+
353
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
354
+
355
+ mgr = EventManager(mock_calendar_manager)
356
+
357
+ # Test with invalid RRULE
358
+ with pytest.raises(EventCreationError) as exc_info:
359
+ mgr.create_event(
360
+ calendar_uid="cal-123",
361
+ summary="Bad Recurring Event",
362
+ start=datetime.now(),
363
+ end=datetime.now() + timedelta(hours=1),
364
+ recurrence_rule="INVALID=RRULE",
365
+ )
366
+
367
+ assert "Invalid RRULE" in str(exc_info.value)
368
+ # Should not have called save_event due to validation failure
369
+ mock_calendar.save_event.assert_not_called()
370
+
371
+ def test_update_event_success(self, mock_calendar_manager, mock_calendar):
372
+ """Test successful event update"""
373
+
374
+ # Setup
375
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
376
+
377
+ # Create mock CalDAV event
378
+ mock_caldav_event = MagicMock()
379
+
380
+ # Create test iCalendar data
381
+ cal = iCalendar()
382
+ event = iEvent()
383
+ event.add("uid", "evt-123")
384
+ event.add("summary", "Original Title")
385
+ event.add("description", "Original Description")
386
+ event.add("dtstart", datetime.now())
387
+ event.add("dtend", datetime.now() + timedelta(hours=1))
388
+ event.add("location", "Original Location")
389
+ cal.add_component(event)
390
+
391
+ mock_caldav_event.data = cal.to_ical().decode("utf-8")
392
+ mock_calendar.event_by_uid.return_value = mock_caldav_event
393
+
394
+ mgr = EventManager(mock_calendar_manager)
395
+
396
+ # Update event
397
+ updated_event = mgr.update_event(
398
+ calendar_uid="cal-123",
399
+ event_uid="evt-123",
400
+ summary="Updated Title",
401
+ description="Updated Description",
402
+ )
403
+
404
+ # Verify update was called
405
+ mock_caldav_event.save.assert_called_once()
406
+
407
+ # Verify the event data was updated
408
+ saved_data = mock_caldav_event.data
409
+ assert "Updated Title" in saved_data
410
+ assert "Updated Description" in saved_data
411
+ assert "Original Location" in saved_data # Unchanged field
412
+
413
+ def test_update_event_partial_update(self, mock_calendar_manager, mock_calendar):
414
+ """Test updating only specific fields"""
415
+
416
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
417
+
418
+ # Create mock CalDAV event with full data
419
+ mock_caldav_event = MagicMock()
420
+ cal = iCalendar()
421
+ event = iEvent()
422
+ event.add("uid", "evt-123")
423
+ event.add("summary", "Original Title")
424
+ event.add("description", "Original Description")
425
+ event.add("dtstart", datetime.now())
426
+ event.add("dtend", datetime.now() + timedelta(hours=1))
427
+ event.add("location", "Conference Room A")
428
+ event.add("rrule", "FREQ=WEEKLY;BYDAY=MO")
429
+ cal.add_component(event)
430
+
431
+ mock_caldav_event.data = cal.to_ical().decode("utf-8")
432
+ mock_calendar.event_by_uid.return_value = mock_caldav_event
433
+
434
+ mgr = EventManager(mock_calendar_manager)
435
+
436
+ # Update only location
437
+ mgr.update_event(
438
+ calendar_uid="cal-123", event_uid="evt-123", location="Conference Room B"
439
+ )
440
+
441
+ # Verify save was called
442
+ mock_caldav_event.save.assert_called_once()
443
+
444
+ # Verify only location changed
445
+ saved_data = mock_caldav_event.data
446
+ assert "Original Title" in saved_data
447
+ assert "Original Description" in saved_data
448
+ assert "Conference Room B" in saved_data
449
+ assert "FREQ=WEEKLY;BYDAY=MO" in saved_data
450
+
451
+ def test_update_event_remove_optional_fields(
452
+ self, mock_calendar_manager, mock_calendar
453
+ ):
454
+ """Test removing optional fields by setting them to empty string"""
455
+
456
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
457
+
458
+ # Create event with optional fields
459
+ mock_caldav_event = MagicMock()
460
+ cal = iCalendar()
461
+ event = iEvent()
462
+ event.add("uid", "evt-123")
463
+ event.add("summary", "Meeting")
464
+ event.add("description", "Team sync")
465
+ event.add("location", "Room 101")
466
+ event.add("dtstart", datetime.now())
467
+ event.add("dtend", datetime.now() + timedelta(hours=1))
468
+ cal.add_component(event)
469
+
470
+ mock_caldav_event.data = cal.to_ical().decode("utf-8")
471
+ mock_calendar.event_by_uid.return_value = mock_caldav_event
472
+
473
+ mgr = EventManager(mock_calendar_manager)
474
+
475
+ # Remove description and location
476
+ mgr.update_event(
477
+ calendar_uid="cal-123",
478
+ event_uid="evt-123",
479
+ description="", # Empty string removes field
480
+ location="", # Empty string removes field
481
+ )
482
+
483
+ saved_data = mock_caldav_event.data
484
+ assert "Meeting" in saved_data # Summary unchanged
485
+ assert "Team sync" not in saved_data # Description removed
486
+ assert "Room 101" not in saved_data # Location removed
487
+
488
+ def test_update_event_not_found(self, mock_calendar_manager, mock_calendar):
489
+ """Test updating non-existent event"""
490
+ from chronos_mcp.exceptions import EventNotFoundError
491
+
492
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
493
+ mock_calendar.event_by_uid.side_effect = Exception("Not found")
494
+ mock_calendar.events.return_value = [] # No events
495
+
496
+ mgr = EventManager(mock_calendar_manager)
497
+
498
+ with pytest.raises(EventNotFoundError) as exc_info:
499
+ mgr.update_event(
500
+ calendar_uid="cal-123", event_uid="non-existent", summary="New Title"
501
+ )
502
+
503
+ assert "non-existent" in str(exc_info.value)
504
+
505
+ def test_update_event_invalid_rrule(self, mock_calendar_manager, mock_calendar):
506
+ """Test updating event with invalid RRULE"""
507
+ from chronos_mcp.exceptions import EventCreationError
508
+
509
+ mock_calendar_manager.get_calendar.return_value = mock_calendar
510
+
511
+ # Create simple event
512
+ mock_caldav_event = MagicMock()
513
+ cal = iCalendar()
514
+ event = iEvent()
515
+ event.add("uid", "evt-123")
516
+ event.add("summary", "Meeting")
517
+ event.add("dtstart", datetime.now())
518
+ event.add("dtend", datetime.now() + timedelta(hours=1))
519
+ cal.add_component(event)
520
+
521
+ mock_caldav_event.data = cal.to_ical().decode("utf-8")
522
+ mock_calendar.event_by_uid.return_value = mock_caldav_event
523
+
524
+ mgr = EventManager(mock_calendar_manager)
525
+
526
+ # Try to update with invalid RRULE
527
+ with pytest.raises(EventCreationError) as exc_info:
528
+ mgr.update_event(
529
+ calendar_uid="cal-123",
530
+ event_uid="evt-123",
531
+ recurrence_rule="INVALID=RRULE",
532
+ )
533
+
534
+ assert "Invalid RRULE" in str(exc_info.value)
535
+ # Verify save was NOT called due to validation failure
536
+ mock_caldav_event.save.assert_not_called()
@@ -0,0 +1,58 @@
1
+ """
2
+ Unit tests for Chronos MCP exception handling framework
3
+ """
4
+
5
+ from chronos_mcp.exceptions import ChronosError
6
+
7
+
8
+ class TestChronosError:
9
+ """Test base ChronosError class"""
10
+
11
+ def test_chronos_error_creation(self):
12
+ """Test creating a ChronosError with all fields"""
13
+ error = ChronosError(
14
+ message="Test error",
15
+ error_code="TEST_ERROR",
16
+ details={"key": "value"},
17
+ request_id="test-123",
18
+ )
19
+
20
+ assert error.message == "Test error"
21
+ assert error.error_code == "TEST_ERROR"
22
+ assert error.details == {"key": "value"}
23
+ assert error.request_id == "test-123"
24
+ assert error.timestamp is not None
25
+ assert error.traceback is not None
26
+
27
+ def test_chronos_error_defaults(self):
28
+ """Test ChronosError with default values"""
29
+ error = ChronosError("Test error")
30
+
31
+ assert error.message == "Test error"
32
+ assert error.error_code == "ChronosError"
33
+ assert error.details == {}
34
+ assert error.request_id is not None # Auto-generated UUID
35
+
36
+ def test_to_dict(self):
37
+ """Test converting error to dictionary"""
38
+ error = ChronosError(
39
+ message="Test error",
40
+ error_code="TEST_ERROR",
41
+ details={"key": "value"},
42
+ request_id="test-123",
43
+ )
44
+
45
+ error_dict = error.to_dict()
46
+ assert error_dict["error"] == "TEST_ERROR"
47
+ assert error_dict["message"] == "Test error"
48
+ assert error_dict["details"] == {"key": "value"}
49
+ assert error_dict["request_id"] == "test-123"
50
+ assert "timestamp" in error_dict
51
+
52
+ def test_str_representation(self):
53
+ """Test string representation of error"""
54
+ error = ChronosError(
55
+ message="Test error", error_code="TEST_ERROR", request_id="test-123"
56
+ )
57
+
58
+ assert str(error) == "TEST_ERROR: Test error (request_id=test-123)"