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,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