iflow-mcp_democratize-technology-chronos-mcp 2.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- chronos_mcp/__init__.py +5 -0
- chronos_mcp/__main__.py +9 -0
- chronos_mcp/accounts.py +410 -0
- chronos_mcp/bulk.py +946 -0
- chronos_mcp/caldav_utils.py +149 -0
- chronos_mcp/calendars.py +204 -0
- chronos_mcp/config.py +187 -0
- chronos_mcp/credentials.py +190 -0
- chronos_mcp/events.py +515 -0
- chronos_mcp/exceptions.py +477 -0
- chronos_mcp/journals.py +477 -0
- chronos_mcp/logging_config.py +23 -0
- chronos_mcp/models.py +202 -0
- chronos_mcp/py.typed +0 -0
- chronos_mcp/rrule.py +259 -0
- chronos_mcp/search.py +315 -0
- chronos_mcp/server.py +121 -0
- chronos_mcp/tasks.py +518 -0
- chronos_mcp/tools/__init__.py +29 -0
- chronos_mcp/tools/accounts.py +151 -0
- chronos_mcp/tools/base.py +59 -0
- chronos_mcp/tools/bulk.py +557 -0
- chronos_mcp/tools/calendars.py +142 -0
- chronos_mcp/tools/events.py +698 -0
- chronos_mcp/tools/journals.py +310 -0
- chronos_mcp/tools/tasks.py +414 -0
- chronos_mcp/utils.py +163 -0
- chronos_mcp/validation.py +636 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/METADATA +299 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/RECORD +68 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/WHEEL +5 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/entry_points.txt +2 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/licenses/LICENSE +21 -0
- iflow_mcp_democratize_technology_chronos_mcp-2.0.0.dist-info/top_level.txt +2 -0
- tests/__init__.py +0 -0
- tests/conftest.py +91 -0
- tests/unit/__init__.py +0 -0
- tests/unit/test_accounts.py +380 -0
- tests/unit/test_accounts_ssrf.py +134 -0
- tests/unit/test_base.py +135 -0
- tests/unit/test_bulk.py +380 -0
- tests/unit/test_bulk_create.py +408 -0
- tests/unit/test_bulk_delete.py +341 -0
- tests/unit/test_bulk_resource_limits.py +74 -0
- tests/unit/test_caldav_utils.py +300 -0
- tests/unit/test_calendars.py +286 -0
- tests/unit/test_config.py +111 -0
- tests/unit/test_config_validation.py +128 -0
- tests/unit/test_credentials_security.py +189 -0
- tests/unit/test_cryptography_security.py +178 -0
- tests/unit/test_events.py +536 -0
- tests/unit/test_exceptions.py +58 -0
- tests/unit/test_journals.py +1097 -0
- tests/unit/test_models.py +95 -0
- tests/unit/test_race_conditions.py +202 -0
- tests/unit/test_recurring_events.py +156 -0
- tests/unit/test_rrule.py +217 -0
- tests/unit/test_search.py +372 -0
- tests/unit/test_search_advanced.py +333 -0
- tests/unit/test_server_input_validation.py +219 -0
- tests/unit/test_ssrf_protection.py +505 -0
- tests/unit/test_tasks.py +918 -0
- tests/unit/test_thread_safety.py +301 -0
- tests/unit/test_tools_journals.py +617 -0
- tests/unit/test_tools_tasks.py +968 -0
- tests/unit/test_url_validation_security.py +234 -0
- tests/unit/test_utils.py +180 -0
- tests/unit/test_validation.py +983 -0
|
@@ -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)"
|