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