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,408 @@
1
+ """
2
+ Unit tests for bulk event creation functionality
3
+ """
4
+
5
+ import json
6
+ from datetime import datetime
7
+ from unittest.mock import Mock, patch
8
+
9
+ import pytest
10
+
11
+ from chronos_mcp.exceptions import ChronosError
12
+
13
+ # Import the actual function directly
14
+ from chronos_mcp.server import bulk_create_events
15
+
16
+
17
+ class TestBulkCreateEvents:
18
+ """Test the bulk_create_events function"""
19
+
20
+ @pytest.fixture
21
+ def mock_managers(self):
22
+ """Setup mock managers"""
23
+ from chronos_mcp.tools.bulk import _managers
24
+
25
+ # Save original state
26
+ original_managers = _managers.copy()
27
+
28
+ # Create mock managers
29
+ mock_bulk = Mock()
30
+ mock_event = Mock()
31
+ mock_logger = Mock()
32
+
33
+ # Set up the global _managers dict
34
+ _managers.clear()
35
+ _managers.update(
36
+ {
37
+ "bulk_manager": mock_bulk,
38
+ "event_manager": mock_event,
39
+ "logger": mock_logger,
40
+ }
41
+ )
42
+
43
+ try:
44
+ yield {"event": mock_event, "bulk": mock_bulk, "logger": mock_logger}
45
+ finally:
46
+ # Restore original state
47
+ _managers.clear()
48
+ _managers.update(original_managers)
49
+
50
+ @pytest.fixture
51
+ def valid_events(self):
52
+ """Valid event data for testing"""
53
+ return [
54
+ {
55
+ "summary": "Event 1",
56
+ "dtstart": "2025-01-20T10:00:00",
57
+ "dtend": "2025-01-20T11:00:00",
58
+ "description": "Test event 1",
59
+ },
60
+ {
61
+ "summary": "Event 2",
62
+ "dtstart": "2025-01-21T14:00:00",
63
+ "dtend": "2025-01-21T15:00:00",
64
+ "location": "Room B",
65
+ },
66
+ {
67
+ "summary": "Event 3",
68
+ "dtstart": "2025-01-22T09:00:00",
69
+ "dtend": "2025-01-22T10:00:00",
70
+ "all_day": False,
71
+ "alarm_minutes": "15",
72
+ },
73
+ ]
74
+
75
+ @pytest.mark.asyncio
76
+ async def test_bulk_create_success(self, mock_managers, valid_events):
77
+ """Test successful bulk creation"""
78
+ # Mock successful bulk creation result
79
+ from chronos_mcp.bulk import BulkResult, OperationResult
80
+
81
+ mock_result = BulkResult(total=3, successful=3, failed=0)
82
+ for i in range(3):
83
+ mock_result.results.append(
84
+ OperationResult(
85
+ index=i, success=True, uid=f"created-{i}", duration_ms=0.1
86
+ )
87
+ )
88
+
89
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
90
+
91
+ result = await bulk_create_events.fn(
92
+ calendar_uid="test-cal",
93
+ events=valid_events,
94
+ mode="continue",
95
+ validate_before_execute=True,
96
+ account=None,
97
+ )
98
+
99
+ assert result["success"] is True
100
+ assert result["total"] == 3
101
+ assert result["succeeded"] == 3
102
+ assert result["failed"] == 0
103
+ assert len(result["details"]) == 3
104
+
105
+ # Check each detail
106
+ for i, detail in enumerate(result["details"]):
107
+ assert detail["success"] is True
108
+ assert detail["uid"] == f"created-{i}"
109
+ assert detail["summary"] == valid_events[i]["summary"]
110
+
111
+ @pytest.mark.asyncio
112
+ async def test_bulk_create_validation_error(self, mock_managers):
113
+ """Test validation errors"""
114
+ invalid_events = [
115
+ {
116
+ "summary": "Valid Event",
117
+ "dtstart": "2025-01-20T10:00:00",
118
+ "dtend": "2025-01-20T11:00:00",
119
+ },
120
+ {
121
+ # Missing summary
122
+ "dtstart": "2025-01-21T14:00:00",
123
+ "dtend": "2025-01-21T15:00:00",
124
+ },
125
+ ]
126
+
127
+ # Mock validation failure result
128
+ from chronos_mcp.bulk import BulkResult, OperationResult
129
+
130
+ mock_result = BulkResult(total=2, successful=1, failed=1)
131
+ mock_result.results.append(
132
+ OperationResult(
133
+ index=0, success=True, uid="valid-event-uid", duration_ms=0.1
134
+ )
135
+ )
136
+ mock_result.results.append(
137
+ OperationResult(
138
+ index=1,
139
+ success=False,
140
+ error="Validation failed: Missing required field: summary",
141
+ duration_ms=0.0,
142
+ )
143
+ )
144
+
145
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
146
+
147
+ # Direct function call
148
+ result = await bulk_create_events.fn(
149
+ calendar_uid="test-cal",
150
+ events=invalid_events,
151
+ mode="continue",
152
+ validate_before_execute=True,
153
+ account=None,
154
+ )
155
+
156
+ assert result["success"] is False
157
+ assert "missing required" in result["details"][1]["error"].lower()
158
+
159
+ @pytest.mark.asyncio
160
+ async def test_bulk_create_continue_mode(self, mock_managers, valid_events):
161
+ """Test continue mode with partial failures"""
162
+
163
+ # Mock mixed success/failure result
164
+ from chronos_mcp.bulk import BulkResult, OperationResult
165
+
166
+ mock_result = BulkResult(total=3, successful=2, failed=1)
167
+ mock_result.results.append(
168
+ OperationResult(index=0, success=True, uid="uid-Event 1", duration_ms=0.1)
169
+ )
170
+ mock_result.results.append(
171
+ OperationResult(
172
+ index=1, success=False, error="Creation failed", duration_ms=0.1
173
+ )
174
+ )
175
+ mock_result.results.append(
176
+ OperationResult(index=2, success=True, uid="uid-Event 3", duration_ms=0.1)
177
+ )
178
+
179
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
180
+
181
+ # Direct function call
182
+ result = await bulk_create_events.fn(
183
+ calendar_uid="test-cal",
184
+ events=valid_events,
185
+ mode="continue",
186
+ validate_before_execute=True,
187
+ account=None,
188
+ )
189
+
190
+ assert result["success"] is False
191
+ assert result["total"] == 3
192
+ assert result["succeeded"] == 2
193
+ assert result["failed"] == 1
194
+
195
+ # Check failed event
196
+ failed = [d for d in result["details"] if not d["success"]][0]
197
+ assert failed["index"] == 1
198
+ assert "Creation failed" in failed["error"]
199
+
200
+ @pytest.mark.asyncio
201
+ async def test_bulk_create_fail_fast_mode(self, mock_managers, valid_events):
202
+ """Test fail_fast mode stops on first error"""
203
+
204
+ # Mock fail_fast result - only first event succeeds, then stops
205
+ from chronos_mcp.bulk import BulkResult, OperationResult
206
+
207
+ mock_result = BulkResult(total=3, successful=1, failed=1)
208
+ mock_result.results.append(
209
+ OperationResult(index=0, success=True, uid="uid-Event 1", duration_ms=0.1)
210
+ )
211
+ mock_result.results.append(
212
+ OperationResult(
213
+ index=1, success=False, error="Creation failed", duration_ms=0.1
214
+ )
215
+ )
216
+ # In fail_fast mode, processing stops after first failure
217
+
218
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
219
+
220
+ # Direct function call
221
+ result = await bulk_create_events.fn(
222
+ calendar_uid="test-cal",
223
+ events=valid_events,
224
+ mode="fail_fast",
225
+ validate_before_execute=True,
226
+ account=None,
227
+ )
228
+
229
+ assert result["success"] is False
230
+ assert result["total"] == 3
231
+ assert result["succeeded"] == 1
232
+ assert result["failed"] == 1
233
+ assert len(result["details"]) == 2 # Stopped after failure
234
+
235
+ @pytest.mark.asyncio
236
+ async def test_bulk_create_invalid_mode(self, mock_managers, valid_events):
237
+ """Test invalid mode validation"""
238
+ # Direct function call
239
+ result = await bulk_create_events.fn(
240
+ calendar_uid="test-cal",
241
+ events=valid_events,
242
+ mode="atomic", # Invalid mode
243
+ validate_before_execute=True,
244
+ account=None,
245
+ )
246
+
247
+ assert result["success"] is False
248
+ assert "Invalid mode" in result["error"]
249
+
250
+ @pytest.mark.asyncio
251
+ async def test_bulk_create_datetime_parsing(self, mock_managers):
252
+ """Test datetime parsing"""
253
+ events = [
254
+ {
255
+ "summary": "Test Event",
256
+ "dtstart": "2025-01-20T10:00:00Z",
257
+ "dtend": "2025-01-20T11:00:00Z",
258
+ }
259
+ ]
260
+
261
+ # Mock successful parsing result
262
+ from chronos_mcp.bulk import BulkResult, OperationResult
263
+
264
+ mock_result = BulkResult(total=1, successful=1, failed=0)
265
+ mock_result.results.append(
266
+ OperationResult(index=0, success=True, uid="test-uid", duration_ms=0.1)
267
+ )
268
+
269
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
270
+
271
+ # Direct function call
272
+ result = await bulk_create_events.fn(
273
+ calendar_uid="test-cal",
274
+ events=events,
275
+ mode="continue",
276
+ validate_before_execute=True,
277
+ account=None,
278
+ )
279
+
280
+ # Check that parsing was successful (no errors)
281
+ assert result["success"] is True
282
+ assert result["succeeded"] == 1
283
+ assert result["failed"] == 0
284
+
285
+ @pytest.mark.asyncio
286
+ async def test_bulk_create_attendees_parsing(self, mock_managers):
287
+ """Test attendees JSON parsing"""
288
+ attendees = [{"email": "test@example.com", "name": "Test User"}]
289
+ events = [
290
+ {
291
+ "summary": "Meeting",
292
+ "dtstart": "2025-01-20T10:00:00",
293
+ "dtend": "2025-01-20T11:00:00",
294
+ "attendees_json": json.dumps(attendees),
295
+ }
296
+ ]
297
+
298
+ # Mock successful parsing result
299
+ from chronos_mcp.bulk import BulkResult, OperationResult
300
+
301
+ mock_result = BulkResult(total=1, successful=1, failed=0)
302
+ mock_result.results.append(
303
+ OperationResult(index=0, success=True, uid="test-uid", duration_ms=0.1)
304
+ )
305
+
306
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
307
+
308
+ # Direct function call
309
+ result = await bulk_create_events.fn(
310
+ calendar_uid="test-cal",
311
+ events=events,
312
+ mode="continue",
313
+ validate_before_execute=True,
314
+ account=None,
315
+ )
316
+
317
+ # Check that parsing was successful (no errors)
318
+ assert result["success"] is True
319
+ assert result["succeeded"] == 1
320
+ assert result["failed"] == 0
321
+
322
+ @pytest.mark.asyncio
323
+ async def test_bulk_create_invalid_attendees_json(self, mock_managers):
324
+ """Test invalid attendees JSON handling"""
325
+ events = [
326
+ {
327
+ "summary": "Meeting",
328
+ "dtstart": "2025-01-20T10:00:00",
329
+ "dtend": "2025-01-20T11:00:00",
330
+ "attendees_json": "not-valid-json",
331
+ }
332
+ ]
333
+
334
+ # Mock result for invalid JSON parsing (should still succeed)
335
+ from chronos_mcp.bulk import BulkResult, OperationResult
336
+
337
+ mock_result = BulkResult(total=1, successful=1, failed=0)
338
+ mock_result.results.append(
339
+ OperationResult(index=0, success=True, uid="test-uid", duration_ms=0.1)
340
+ )
341
+
342
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
343
+
344
+ # Direct function call
345
+ result = await bulk_create_events.fn(
346
+ calendar_uid="test-cal",
347
+ events=events,
348
+ mode="continue",
349
+ validate_before_execute=True,
350
+ account=None,
351
+ )
352
+
353
+ # Invalid JSON is ignored, event still created successfully
354
+ assert result["succeeded"] == 1
355
+ assert result["failed"] == 0
356
+
357
+ @pytest.mark.asyncio
358
+ async def test_bulk_create_alarm_parsing(self, mock_managers):
359
+ """Test alarm minutes parsing"""
360
+ events = [
361
+ {
362
+ "summary": "Meeting",
363
+ "dtstart": "2025-01-20T10:00:00",
364
+ "dtend": "2025-01-20T11:00:00",
365
+ "alarm_minutes": "30",
366
+ }
367
+ ]
368
+
369
+ # Mock successful parsing result
370
+ from chronos_mcp.bulk import BulkResult, OperationResult
371
+
372
+ mock_result = BulkResult(total=1, successful=1, failed=0)
373
+ mock_result.results.append(
374
+ OperationResult(index=0, success=True, uid="test-uid", duration_ms=0.1)
375
+ )
376
+
377
+ mock_managers["bulk"].bulk_create_events.return_value = mock_result
378
+
379
+ # Direct function call
380
+ result = await bulk_create_events.fn(
381
+ calendar_uid="test-cal",
382
+ events=events,
383
+ mode="continue",
384
+ validate_before_execute=True,
385
+ account=None,
386
+ )
387
+
388
+ # Check that parsing was successful (no errors)
389
+ assert result["success"] is True
390
+ assert result["succeeded"] == 1
391
+ assert result["failed"] == 0
392
+
393
+ @pytest.mark.asyncio
394
+ async def test_bulk_create_empty_list(self, mock_managers):
395
+ """Test empty event list"""
396
+ # Direct function call
397
+ result = await bulk_create_events.fn(
398
+ calendar_uid="test-cal",
399
+ events=[],
400
+ mode="continue",
401
+ validate_before_execute=True,
402
+ account=None,
403
+ )
404
+
405
+ assert result["success"] is True
406
+ assert result["total"] == 0
407
+ assert result["succeeded"] == 0
408
+ assert result["failed"] == 0