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,300 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for chronos_mcp/caldav_utils.py
|
|
3
|
+
|
|
4
|
+
Tests the get_item_with_fallback utility function that eliminates
|
|
5
|
+
8x code duplication across events, tasks, and journals managers.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import pytest
|
|
9
|
+
from unittest.mock import Mock, MagicMock
|
|
10
|
+
from icalendar import Calendar as iCalendar, Event as iEvent, Todo as iTodo, Journal as iJournal
|
|
11
|
+
|
|
12
|
+
from chronos_mcp.caldav_utils import get_item_with_fallback
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class TestGetItemWithFallback:
|
|
16
|
+
"""Test get_item_with_fallback function"""
|
|
17
|
+
|
|
18
|
+
@pytest.fixture
|
|
19
|
+
def mock_calendar(self):
|
|
20
|
+
"""Create a mock calendar object"""
|
|
21
|
+
calendar = Mock()
|
|
22
|
+
return calendar
|
|
23
|
+
|
|
24
|
+
@pytest.fixture
|
|
25
|
+
def mock_event_item(self):
|
|
26
|
+
"""Create a mock CalDAV event item"""
|
|
27
|
+
item = Mock()
|
|
28
|
+
# Create iCalendar data with VEVENT component
|
|
29
|
+
cal = iCalendar()
|
|
30
|
+
event = iEvent()
|
|
31
|
+
event.add("uid", "event-123")
|
|
32
|
+
event.add("summary", "Test Event")
|
|
33
|
+
cal.add_component(event)
|
|
34
|
+
item.data = cal.to_ical()
|
|
35
|
+
return item
|
|
36
|
+
|
|
37
|
+
@pytest.fixture
|
|
38
|
+
def mock_task_item(self):
|
|
39
|
+
"""Create a mock CalDAV task item"""
|
|
40
|
+
item = Mock()
|
|
41
|
+
# Create iCalendar data with VTODO component
|
|
42
|
+
cal = iCalendar()
|
|
43
|
+
todo = iTodo()
|
|
44
|
+
todo.add("uid", "task-456")
|
|
45
|
+
todo.add("summary", "Test Task")
|
|
46
|
+
cal.add_component(todo)
|
|
47
|
+
item.data = cal.to_ical()
|
|
48
|
+
return item
|
|
49
|
+
|
|
50
|
+
@pytest.fixture
|
|
51
|
+
def mock_journal_item(self):
|
|
52
|
+
"""Create a mock CalDAV journal item"""
|
|
53
|
+
item = Mock()
|
|
54
|
+
# Create iCalendar data with VJOURNAL component
|
|
55
|
+
cal = iCalendar()
|
|
56
|
+
journal = iJournal()
|
|
57
|
+
journal.add("uid", "journal-789")
|
|
58
|
+
journal.add("summary", "Test Journal")
|
|
59
|
+
cal.add_component(journal)
|
|
60
|
+
item.data = cal.to_ical()
|
|
61
|
+
return item
|
|
62
|
+
|
|
63
|
+
# METHOD 1 SUCCESS TESTS (Direct UID lookup)
|
|
64
|
+
|
|
65
|
+
def test_event_method1_success(self, mock_calendar, mock_event_item):
|
|
66
|
+
"""Test event found using event_by_uid (Method 1)"""
|
|
67
|
+
mock_calendar.event_by_uid = Mock(return_value=mock_event_item)
|
|
68
|
+
|
|
69
|
+
result = get_item_with_fallback(mock_calendar, "event-123", "event")
|
|
70
|
+
|
|
71
|
+
assert result == mock_event_item
|
|
72
|
+
mock_calendar.event_by_uid.assert_called_once_with("event-123")
|
|
73
|
+
|
|
74
|
+
def test_task_method1_success(self, mock_calendar, mock_task_item):
|
|
75
|
+
"""Test task found using event_by_uid (Method 1)"""
|
|
76
|
+
mock_calendar.event_by_uid = Mock(return_value=mock_task_item)
|
|
77
|
+
|
|
78
|
+
result = get_item_with_fallback(mock_calendar, "task-456", "task")
|
|
79
|
+
|
|
80
|
+
assert result == mock_task_item
|
|
81
|
+
mock_calendar.event_by_uid.assert_called_once_with("task-456")
|
|
82
|
+
|
|
83
|
+
def test_journal_method1_success(self, mock_calendar, mock_journal_item):
|
|
84
|
+
"""Test journal found using event_by_uid (Method 1)"""
|
|
85
|
+
mock_calendar.event_by_uid = Mock(return_value=mock_journal_item)
|
|
86
|
+
|
|
87
|
+
result = get_item_with_fallback(mock_calendar, "journal-789", "journal")
|
|
88
|
+
|
|
89
|
+
assert result == mock_journal_item
|
|
90
|
+
mock_calendar.event_by_uid.assert_called_once_with("journal-789")
|
|
91
|
+
|
|
92
|
+
# METHOD 2 FALLBACK TESTS (Iterate and search)
|
|
93
|
+
|
|
94
|
+
def test_event_method2_fallback(self, mock_calendar, mock_event_item):
|
|
95
|
+
"""Test event found using fallback search (Method 2)"""
|
|
96
|
+
# Method 1 fails
|
|
97
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
98
|
+
# Method 2 succeeds
|
|
99
|
+
mock_calendar.events = Mock(return_value=[mock_event_item])
|
|
100
|
+
|
|
101
|
+
result = get_item_with_fallback(mock_calendar, "event-123", "event")
|
|
102
|
+
|
|
103
|
+
assert result == mock_event_item
|
|
104
|
+
mock_calendar.event_by_uid.assert_called_once()
|
|
105
|
+
mock_calendar.events.assert_called_once()
|
|
106
|
+
|
|
107
|
+
def test_task_method2_fallback(self, mock_calendar, mock_task_item):
|
|
108
|
+
"""Test task found using fallback search (Method 2)"""
|
|
109
|
+
# Method 1 fails
|
|
110
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
111
|
+
# Method 2 succeeds
|
|
112
|
+
mock_calendar.todos = Mock(return_value=[mock_task_item])
|
|
113
|
+
|
|
114
|
+
result = get_item_with_fallback(mock_calendar, "task-456", "task")
|
|
115
|
+
|
|
116
|
+
assert result == mock_task_item
|
|
117
|
+
mock_calendar.event_by_uid.assert_called_once()
|
|
118
|
+
mock_calendar.todos.assert_called_once()
|
|
119
|
+
|
|
120
|
+
def test_journal_method2_fallback(self, mock_calendar, mock_journal_item):
|
|
121
|
+
"""Test journal found using fallback search (Method 2)"""
|
|
122
|
+
# Method 1 fails
|
|
123
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
124
|
+
# Method 2 succeeds
|
|
125
|
+
mock_calendar.journals = Mock(return_value=[mock_journal_item])
|
|
126
|
+
|
|
127
|
+
result = get_item_with_fallback(mock_calendar, "journal-789", "journal")
|
|
128
|
+
|
|
129
|
+
assert result == mock_journal_item
|
|
130
|
+
mock_calendar.event_by_uid.assert_called_once()
|
|
131
|
+
mock_calendar.journals.assert_called_once()
|
|
132
|
+
|
|
133
|
+
# FALLBACK METHOD TESTS (todos/journals not available)
|
|
134
|
+
|
|
135
|
+
def test_task_fallback_to_events(self, mock_calendar, mock_task_item):
|
|
136
|
+
"""Test task search falls back to events() when todos() not available"""
|
|
137
|
+
# Explicitly configure calendar methods
|
|
138
|
+
del mock_calendar.event_by_uid # Method 1 not available
|
|
139
|
+
del mock_calendar.todos # Primary method not available
|
|
140
|
+
mock_calendar.events = Mock(return_value=[mock_task_item])
|
|
141
|
+
|
|
142
|
+
result = get_item_with_fallback(mock_calendar, "task-456", "task")
|
|
143
|
+
|
|
144
|
+
assert result == mock_task_item
|
|
145
|
+
mock_calendar.events.assert_called_once()
|
|
146
|
+
|
|
147
|
+
def test_journal_fallback_to_events(self, mock_calendar, mock_journal_item):
|
|
148
|
+
"""Test journal search falls back to events() when journals() not available"""
|
|
149
|
+
# Explicitly configure calendar methods
|
|
150
|
+
del mock_calendar.event_by_uid # Method 1 not available
|
|
151
|
+
del mock_calendar.journals # Primary method not available
|
|
152
|
+
mock_calendar.events = Mock(return_value=[mock_journal_item])
|
|
153
|
+
|
|
154
|
+
result = get_item_with_fallback(mock_calendar, "journal-789", "journal")
|
|
155
|
+
|
|
156
|
+
assert result == mock_journal_item
|
|
157
|
+
mock_calendar.events.assert_called_once()
|
|
158
|
+
|
|
159
|
+
# MULTIPLE ITEMS TESTS (search through list)
|
|
160
|
+
|
|
161
|
+
def test_event_found_in_multiple_items(self, mock_calendar):
|
|
162
|
+
"""Test finding specific event among multiple items"""
|
|
163
|
+
# Create multiple event items
|
|
164
|
+
items = []
|
|
165
|
+
for i in range(3):
|
|
166
|
+
item = Mock()
|
|
167
|
+
cal = iCalendar()
|
|
168
|
+
event = iEvent()
|
|
169
|
+
event.add("uid", f"event-{i}")
|
|
170
|
+
event.add("summary", f"Event {i}")
|
|
171
|
+
cal.add_component(event)
|
|
172
|
+
item.data = cal.to_ical()
|
|
173
|
+
items.append(item)
|
|
174
|
+
|
|
175
|
+
# Method 1 fails
|
|
176
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
177
|
+
# Method 2 with multiple items
|
|
178
|
+
mock_calendar.events = Mock(return_value=items)
|
|
179
|
+
|
|
180
|
+
# Find the middle item
|
|
181
|
+
result = get_item_with_fallback(mock_calendar, "event-1", "event")
|
|
182
|
+
|
|
183
|
+
assert result == items[1]
|
|
184
|
+
|
|
185
|
+
# ERROR CASES
|
|
186
|
+
|
|
187
|
+
def test_invalid_item_type(self, mock_calendar):
|
|
188
|
+
"""Test ValueError raised for invalid item type"""
|
|
189
|
+
with pytest.raises(ValueError, match="Invalid item_type"):
|
|
190
|
+
get_item_with_fallback(mock_calendar, "test-123", "invalid_type")
|
|
191
|
+
|
|
192
|
+
def test_item_not_found(self, mock_calendar, mock_event_item):
|
|
193
|
+
"""Test ValueError raised when item not found"""
|
|
194
|
+
# Method 1 fails
|
|
195
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
196
|
+
# Method 2 returns different UID
|
|
197
|
+
wrong_item = Mock()
|
|
198
|
+
cal = iCalendar()
|
|
199
|
+
event = iEvent()
|
|
200
|
+
event.add("uid", "different-uid")
|
|
201
|
+
cal.add_component(event)
|
|
202
|
+
wrong_item.data = cal.to_ical()
|
|
203
|
+
mock_calendar.events = Mock(return_value=[wrong_item])
|
|
204
|
+
|
|
205
|
+
with pytest.raises(ValueError, match="Event with UID 'event-123' not found"):
|
|
206
|
+
get_item_with_fallback(mock_calendar, "event-123", "event")
|
|
207
|
+
|
|
208
|
+
def test_no_list_method_available(self, mock_calendar):
|
|
209
|
+
"""Test ValueError when no list method available"""
|
|
210
|
+
# Explicitly configure calendar to have no methods
|
|
211
|
+
del mock_calendar.event_by_uid # Method 1 not available
|
|
212
|
+
del mock_calendar.todos # Primary list method not available
|
|
213
|
+
del mock_calendar.events # Fallback method not available
|
|
214
|
+
|
|
215
|
+
with pytest.raises(ValueError, match="does not support"):
|
|
216
|
+
get_item_with_fallback(mock_calendar, "task-456", "task")
|
|
217
|
+
|
|
218
|
+
def test_parse_error_handling(self, mock_calendar):
|
|
219
|
+
"""Test graceful handling of iCalendar parse errors"""
|
|
220
|
+
# Method 1 fails
|
|
221
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
222
|
+
|
|
223
|
+
# Create item with malformed data
|
|
224
|
+
bad_item = Mock()
|
|
225
|
+
bad_item.data = b"MALFORMED DATA"
|
|
226
|
+
|
|
227
|
+
# Create valid item
|
|
228
|
+
good_item = Mock()
|
|
229
|
+
cal = iCalendar()
|
|
230
|
+
event = iEvent()
|
|
231
|
+
event.add("uid", "event-123")
|
|
232
|
+
cal.add_component(event)
|
|
233
|
+
good_item.data = cal.to_ical()
|
|
234
|
+
|
|
235
|
+
mock_calendar.events = Mock(return_value=[bad_item, good_item])
|
|
236
|
+
|
|
237
|
+
# Should skip bad item and find good item
|
|
238
|
+
result = get_item_with_fallback(mock_calendar, "event-123", "event")
|
|
239
|
+
assert result == good_item
|
|
240
|
+
|
|
241
|
+
# REQUEST_ID LOGGING TESTS
|
|
242
|
+
|
|
243
|
+
def test_request_id_passed_to_logging(self, mock_calendar, mock_event_item, caplog):
|
|
244
|
+
"""Test request_id is passed to logging context"""
|
|
245
|
+
mock_calendar.event_by_uid = Mock(return_value=mock_event_item)
|
|
246
|
+
|
|
247
|
+
get_item_with_fallback(mock_calendar, "event-123", "event", request_id="req-789")
|
|
248
|
+
|
|
249
|
+
# Verify request_id appears in logs (implementation detail, but validates parameter is used)
|
|
250
|
+
# Note: actual log verification would require caplog fixture and checking extra fields
|
|
251
|
+
|
|
252
|
+
# UID MATCHING TESTS (string exact match)
|
|
253
|
+
|
|
254
|
+
def test_uid_exact_match_required(self, mock_calendar):
|
|
255
|
+
"""Test UID matching requires exact string match"""
|
|
256
|
+
# Method 1 fails
|
|
257
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
258
|
+
|
|
259
|
+
# Create item with similar but not exact UID
|
|
260
|
+
item = Mock()
|
|
261
|
+
cal = iCalendar()
|
|
262
|
+
event = iEvent()
|
|
263
|
+
event.add("uid", "event-123-extra") # Similar but not exact
|
|
264
|
+
cal.add_component(event)
|
|
265
|
+
item.data = cal.to_ical()
|
|
266
|
+
mock_calendar.events = Mock(return_value=[item])
|
|
267
|
+
|
|
268
|
+
# Should not match
|
|
269
|
+
with pytest.raises(ValueError, match="not found"):
|
|
270
|
+
get_item_with_fallback(mock_calendar, "event-123", "event")
|
|
271
|
+
|
|
272
|
+
def test_uid_in_data_fast_check(self, mock_calendar):
|
|
273
|
+
"""Test fast UID check in raw data before parsing"""
|
|
274
|
+
# Method 1 fails
|
|
275
|
+
mock_calendar.event_by_uid = Mock(side_effect=Exception("Not found"))
|
|
276
|
+
|
|
277
|
+
# Create items where UID is not in data
|
|
278
|
+
items_without_uid = []
|
|
279
|
+
for i in range(100): # Many items to test performance
|
|
280
|
+
item = Mock()
|
|
281
|
+
cal = iCalendar()
|
|
282
|
+
event = iEvent()
|
|
283
|
+
event.add("uid", f"other-{i}")
|
|
284
|
+
cal.add_component(event)
|
|
285
|
+
item.data = cal.to_ical()
|
|
286
|
+
items_without_uid.append(item)
|
|
287
|
+
|
|
288
|
+
# Add the target item at the end
|
|
289
|
+
target_item = Mock()
|
|
290
|
+
cal = iCalendar()
|
|
291
|
+
event = iEvent()
|
|
292
|
+
event.add("uid", "event-123")
|
|
293
|
+
cal.add_component(event)
|
|
294
|
+
target_item.data = cal.to_ical()
|
|
295
|
+
items_without_uid.append(target_item)
|
|
296
|
+
|
|
297
|
+
mock_calendar.events = Mock(return_value=items_without_uid)
|
|
298
|
+
|
|
299
|
+
result = get_item_with_fallback(mock_calendar, "event-123", "event")
|
|
300
|
+
assert result == target_item
|
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unit tests for calendar management
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
from unittest.mock import Mock
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
from chronos_mcp.accounts import AccountManager
|
|
10
|
+
from chronos_mcp.calendars import CalendarManager
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class TestCalendarManager:
|
|
14
|
+
"""Test calendar management functionality"""
|
|
15
|
+
|
|
16
|
+
@pytest.fixture
|
|
17
|
+
def mock_account_manager(self):
|
|
18
|
+
"""Mock AccountManager"""
|
|
19
|
+
mock = Mock(spec=AccountManager)
|
|
20
|
+
mock.config = Mock()
|
|
21
|
+
mock.config.config = Mock()
|
|
22
|
+
mock.config.config.default_account = "default"
|
|
23
|
+
return mock
|
|
24
|
+
|
|
25
|
+
@pytest.fixture
|
|
26
|
+
def mock_principal(self):
|
|
27
|
+
"""Mock CalDAV principal"""
|
|
28
|
+
principal = Mock()
|
|
29
|
+
return principal
|
|
30
|
+
|
|
31
|
+
@pytest.fixture
|
|
32
|
+
def mock_calendar(self):
|
|
33
|
+
"""Mock CalDAV calendar"""
|
|
34
|
+
cal = Mock()
|
|
35
|
+
cal.url = "http://caldav.example.com/calendars/user/test-calendar/"
|
|
36
|
+
cal.name = "Test Calendar"
|
|
37
|
+
return cal
|
|
38
|
+
|
|
39
|
+
def test_init(self, mock_account_manager):
|
|
40
|
+
"""Test CalendarManager initialization"""
|
|
41
|
+
mgr = CalendarManager(mock_account_manager)
|
|
42
|
+
assert mgr.accounts == mock_account_manager
|
|
43
|
+
|
|
44
|
+
def test_list_calendars_no_principal(self, mock_account_manager):
|
|
45
|
+
"""Test listing calendars when no principal found"""
|
|
46
|
+
mock_account_manager.get_principal.return_value = None
|
|
47
|
+
|
|
48
|
+
mgr = CalendarManager(mock_account_manager)
|
|
49
|
+
|
|
50
|
+
# Should raise AccountNotFoundError when no principal
|
|
51
|
+
from chronos_mcp.exceptions import AccountNotFoundError
|
|
52
|
+
|
|
53
|
+
with pytest.raises(AccountNotFoundError) as exc_info:
|
|
54
|
+
mgr.list_calendars("test_account")
|
|
55
|
+
|
|
56
|
+
assert "test_account" in str(exc_info.value)
|
|
57
|
+
|
|
58
|
+
def test_list_calendars_success(
|
|
59
|
+
self, mock_account_manager, mock_principal, mock_calendar
|
|
60
|
+
):
|
|
61
|
+
"""Test successful calendar listing"""
|
|
62
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
63
|
+
mock_calendar2 = Mock()
|
|
64
|
+
mock_calendar2.url = "http://caldav.example.com/calendars/user/personal"
|
|
65
|
+
mock_calendar2.name = "Personal"
|
|
66
|
+
|
|
67
|
+
mock_principal.calendars.return_value = [mock_calendar, mock_calendar2]
|
|
68
|
+
|
|
69
|
+
mgr = CalendarManager(mock_account_manager)
|
|
70
|
+
result = mgr.list_calendars("test_account")
|
|
71
|
+
|
|
72
|
+
assert len(result) == 2
|
|
73
|
+
assert result[0].uid == "test-calendar"
|
|
74
|
+
assert result[0].name == "Test Calendar"
|
|
75
|
+
assert result[0].account_alias == "test_account"
|
|
76
|
+
assert result[1].uid == "personal"
|
|
77
|
+
assert result[1].name == "Personal"
|
|
78
|
+
|
|
79
|
+
def test_list_calendars_with_default_account(
|
|
80
|
+
self, mock_account_manager, mock_principal, mock_calendar
|
|
81
|
+
):
|
|
82
|
+
"""Test listing calendars with default account"""
|
|
83
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
84
|
+
mock_principal.calendars.return_value = [mock_calendar]
|
|
85
|
+
|
|
86
|
+
mgr = CalendarManager(mock_account_manager)
|
|
87
|
+
result = mgr.list_calendars() # No account specified
|
|
88
|
+
|
|
89
|
+
assert len(result) == 1
|
|
90
|
+
assert result[0].account_alias == "default"
|
|
91
|
+
|
|
92
|
+
def test_list_calendars_exception(self, mock_account_manager, mock_principal):
|
|
93
|
+
"""Test calendar listing with exception"""
|
|
94
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
95
|
+
mock_principal.calendars.side_effect = Exception("CalDAV error")
|
|
96
|
+
|
|
97
|
+
mgr = CalendarManager(mock_account_manager)
|
|
98
|
+
result = mgr.list_calendars("test_account")
|
|
99
|
+
|
|
100
|
+
assert result == []
|
|
101
|
+
|
|
102
|
+
def test_create_calendar_no_principal(self, mock_account_manager):
|
|
103
|
+
"""Test creating calendar when no principal found"""
|
|
104
|
+
mock_account_manager.get_principal.return_value = None
|
|
105
|
+
|
|
106
|
+
mgr = CalendarManager(mock_account_manager)
|
|
107
|
+
|
|
108
|
+
# Should raise AccountNotFoundError
|
|
109
|
+
from chronos_mcp.exceptions import AccountNotFoundError
|
|
110
|
+
|
|
111
|
+
with pytest.raises(AccountNotFoundError) as exc_info:
|
|
112
|
+
mgr.create_calendar("New Calendar", account_alias="test_account")
|
|
113
|
+
|
|
114
|
+
assert "test_account" in str(exc_info.value)
|
|
115
|
+
mock_account_manager.get_principal.assert_called_once_with("test_account")
|
|
116
|
+
|
|
117
|
+
def test_create_calendar_success(self, mock_account_manager, mock_principal):
|
|
118
|
+
"""Test successful calendar creation"""
|
|
119
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
120
|
+
|
|
121
|
+
# Mock the created calendar
|
|
122
|
+
created_cal = Mock()
|
|
123
|
+
created_cal.url = "http://caldav.example.com/calendars/user/new_calendar/"
|
|
124
|
+
mock_principal.make_calendar.return_value = created_cal
|
|
125
|
+
|
|
126
|
+
mgr = CalendarManager(mock_account_manager)
|
|
127
|
+
result = mgr.create_calendar(
|
|
128
|
+
name="New Calendar",
|
|
129
|
+
description="Test Description",
|
|
130
|
+
color="#FF0000",
|
|
131
|
+
account_alias="test_account",
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
assert result is not None
|
|
135
|
+
assert result.uid == "new_calendar"
|
|
136
|
+
assert result.name == "New Calendar"
|
|
137
|
+
assert result.description == "Test Description"
|
|
138
|
+
assert result.color == "#FF0000"
|
|
139
|
+
assert result.account_alias == "test_account"
|
|
140
|
+
assert result.read_only is False
|
|
141
|
+
|
|
142
|
+
mock_principal.make_calendar.assert_called_once_with(
|
|
143
|
+
name="New Calendar", cal_id="new_calendar"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
def test_create_calendar_with_default_account(
|
|
147
|
+
self, mock_account_manager, mock_principal
|
|
148
|
+
):
|
|
149
|
+
"""Test creating calendar with default account"""
|
|
150
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
151
|
+
created_cal = Mock()
|
|
152
|
+
created_cal.url = "http://caldav.example.com/calendars/user/test_cal/"
|
|
153
|
+
mock_principal.make_calendar.return_value = created_cal
|
|
154
|
+
|
|
155
|
+
mgr = CalendarManager(mock_account_manager)
|
|
156
|
+
result = mgr.create_calendar("Test Cal") # No account specified
|
|
157
|
+
|
|
158
|
+
assert result is not None
|
|
159
|
+
assert result.account_alias == "default"
|
|
160
|
+
|
|
161
|
+
def test_create_calendar_exception(self, mock_account_manager, mock_principal):
|
|
162
|
+
"""Test calendar creation with exception"""
|
|
163
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
164
|
+
mock_principal.make_calendar.side_effect = Exception("CalDAV error")
|
|
165
|
+
|
|
166
|
+
mgr = CalendarManager(mock_account_manager)
|
|
167
|
+
|
|
168
|
+
# Should raise CalendarCreationError
|
|
169
|
+
from chronos_mcp.exceptions import CalendarCreationError
|
|
170
|
+
|
|
171
|
+
with pytest.raises(CalendarCreationError) as exc_info:
|
|
172
|
+
mgr.create_calendar("New Calendar")
|
|
173
|
+
|
|
174
|
+
assert "CalDAV error" in str(exc_info.value)
|
|
175
|
+
|
|
176
|
+
def test_delete_calendar_no_principal(self, mock_account_manager):
|
|
177
|
+
"""Test deleting calendar when no principal found"""
|
|
178
|
+
mock_account_manager.get_principal.return_value = None
|
|
179
|
+
|
|
180
|
+
mgr = CalendarManager(mock_account_manager)
|
|
181
|
+
|
|
182
|
+
# Should raise AccountNotFoundError
|
|
183
|
+
from chronos_mcp.exceptions import AccountNotFoundError
|
|
184
|
+
|
|
185
|
+
with pytest.raises(AccountNotFoundError) as exc_info:
|
|
186
|
+
mgr.delete_calendar("cal-123", "test_account")
|
|
187
|
+
|
|
188
|
+
assert "test_account" in str(exc_info.value)
|
|
189
|
+
mock_account_manager.get_principal.assert_called_once_with("test_account")
|
|
190
|
+
|
|
191
|
+
def test_delete_calendar_success(
|
|
192
|
+
self, mock_account_manager, mock_principal, mock_calendar
|
|
193
|
+
):
|
|
194
|
+
"""Test successful calendar deletion"""
|
|
195
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
196
|
+
mock_principal.calendars.return_value = [mock_calendar]
|
|
197
|
+
|
|
198
|
+
mgr = CalendarManager(mock_account_manager)
|
|
199
|
+
result = mgr.delete_calendar("test-calendar", "test_account")
|
|
200
|
+
|
|
201
|
+
assert result is True
|
|
202
|
+
mock_calendar.delete.assert_called_once()
|
|
203
|
+
|
|
204
|
+
def test_delete_calendar_not_found(self, mock_account_manager, mock_principal):
|
|
205
|
+
"""Test deleting non-existent calendar"""
|
|
206
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
207
|
+
|
|
208
|
+
# Mock calendar with different UID
|
|
209
|
+
other_cal = Mock()
|
|
210
|
+
other_cal.url = "http://caldav.example.com/calendars/user/other-cal/"
|
|
211
|
+
mock_principal.calendars.return_value = [other_cal]
|
|
212
|
+
|
|
213
|
+
mgr = CalendarManager(mock_account_manager)
|
|
214
|
+
|
|
215
|
+
# Should raise CalendarNotFoundError
|
|
216
|
+
from chronos_mcp.exceptions import CalendarNotFoundError
|
|
217
|
+
|
|
218
|
+
with pytest.raises(CalendarNotFoundError) as exc_info:
|
|
219
|
+
mgr.delete_calendar("test-calendar", "test_account")
|
|
220
|
+
|
|
221
|
+
assert "test-calendar" in str(exc_info.value)
|
|
222
|
+
other_cal.delete.assert_not_called()
|
|
223
|
+
|
|
224
|
+
def test_delete_calendar_exception(
|
|
225
|
+
self, mock_account_manager, mock_principal, mock_calendar
|
|
226
|
+
):
|
|
227
|
+
"""Test calendar deletion with exception"""
|
|
228
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
229
|
+
mock_principal.calendars.return_value = [mock_calendar]
|
|
230
|
+
mock_calendar.delete.side_effect = Exception("CalDAV error")
|
|
231
|
+
|
|
232
|
+
mgr = CalendarManager(mock_account_manager)
|
|
233
|
+
|
|
234
|
+
# Should raise CalendarDeletionError
|
|
235
|
+
from chronos_mcp.exceptions import CalendarDeletionError
|
|
236
|
+
|
|
237
|
+
with pytest.raises(CalendarDeletionError) as exc_info:
|
|
238
|
+
mgr.delete_calendar("test-calendar", "test_account")
|
|
239
|
+
|
|
240
|
+
assert "CalDAV error" in str(exc_info.value)
|
|
241
|
+
|
|
242
|
+
def test_get_calendar_no_principal(self, mock_account_manager):
|
|
243
|
+
"""Test getting calendar when no principal found"""
|
|
244
|
+
mock_account_manager.get_principal.return_value = None
|
|
245
|
+
|
|
246
|
+
mgr = CalendarManager(mock_account_manager)
|
|
247
|
+
result = mgr.get_calendar("cal-123", "test_account")
|
|
248
|
+
|
|
249
|
+
assert result is None
|
|
250
|
+
mock_account_manager.get_principal.assert_called_once_with("test_account")
|
|
251
|
+
|
|
252
|
+
def test_get_calendar_success(
|
|
253
|
+
self, mock_account_manager, mock_principal, mock_calendar
|
|
254
|
+
):
|
|
255
|
+
"""Test successful calendar retrieval"""
|
|
256
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
257
|
+
mock_principal.calendars.return_value = [mock_calendar]
|
|
258
|
+
|
|
259
|
+
mgr = CalendarManager(mock_account_manager)
|
|
260
|
+
result = mgr.get_calendar("test-calendar", "test_account")
|
|
261
|
+
|
|
262
|
+
assert result == mock_calendar
|
|
263
|
+
|
|
264
|
+
def test_get_calendar_not_found(self, mock_account_manager, mock_principal):
|
|
265
|
+
"""Test getting non-existent calendar"""
|
|
266
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
267
|
+
|
|
268
|
+
# Mock calendar with different UID
|
|
269
|
+
other_cal = Mock()
|
|
270
|
+
other_cal.url = "http://caldav.example.com/calendars/user/other-cal/"
|
|
271
|
+
mock_principal.calendars.return_value = [other_cal]
|
|
272
|
+
|
|
273
|
+
mgr = CalendarManager(mock_account_manager)
|
|
274
|
+
result = mgr.get_calendar("test-calendar", "test_account")
|
|
275
|
+
|
|
276
|
+
assert result is None
|
|
277
|
+
|
|
278
|
+
def test_get_calendar_exception(self, mock_account_manager, mock_principal):
|
|
279
|
+
"""Test getting calendar with exception"""
|
|
280
|
+
mock_account_manager.get_principal.return_value = mock_principal
|
|
281
|
+
mock_principal.calendars.side_effect = Exception("CalDAV error")
|
|
282
|
+
|
|
283
|
+
mgr = CalendarManager(mock_account_manager)
|
|
284
|
+
result = mgr.get_calendar("test-calendar", "test_account")
|
|
285
|
+
|
|
286
|
+
assert result is None
|