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,301 @@
1
+ """
2
+ Thread safety tests for connection management
3
+ """
4
+
5
+ import threading
6
+ import time
7
+ from unittest.mock import MagicMock, Mock, patch
8
+
9
+ import pytest
10
+
11
+ from chronos_mcp.accounts import AccountManager
12
+ from chronos_mcp.config import ConfigManager
13
+ from chronos_mcp.models import Account, AccountStatus
14
+
15
+
16
+ class TestThreadSafety:
17
+ """Test thread safety of connection management"""
18
+
19
+ @pytest.fixture
20
+ def mock_config_with_account(self):
21
+ """Create a mock config manager with a test account"""
22
+ config_manager = Mock(spec=ConfigManager)
23
+
24
+ # Create a test account
25
+ test_account = Account(
26
+ alias="test_account",
27
+ url="https://caldav.example.com/",
28
+ username="testuser",
29
+ password="testpass",
30
+ display_name="Test Account",
31
+ )
32
+
33
+ config_manager.get_account.return_value = test_account
34
+
35
+ # Mock the config attribute and its default_account
36
+ mock_config = Mock()
37
+ mock_config.default_account = "test_account"
38
+ config_manager.config = mock_config
39
+
40
+ return config_manager
41
+
42
+ def test_concurrent_connection_creation(self, mock_config_with_account):
43
+ """Test that concurrent connection attempts don't create duplicate connections"""
44
+ with (
45
+ patch("chronos_mcp.accounts.DAVClient") as mock_dav_client,
46
+ patch("chronos_mcp.accounts.get_credential_manager") as mock_cred_mgr,
47
+ ):
48
+
49
+ # Setup mocks
50
+ mock_client = Mock()
51
+ mock_principal = Mock()
52
+ mock_client.principal.return_value = mock_principal
53
+ mock_dav_client.return_value = mock_client
54
+
55
+ mock_cred_mgr.return_value.get_password.return_value = "testpass"
56
+
57
+ manager = AccountManager(mock_config_with_account)
58
+
59
+ # Track connection attempts
60
+ connection_attempts = []
61
+ original_connect = manager.connect_account
62
+
63
+ def track_connect(alias, request_id=None):
64
+ connection_attempts.append(time.time())
65
+ # Add small delay to increase chance of race condition
66
+ time.sleep(0.01)
67
+ return original_connect(alias, request_id)
68
+
69
+ manager.connect_account = track_connect
70
+
71
+ # Create multiple threads that try to get the same connection
72
+ threads = []
73
+ results = []
74
+
75
+ def get_connection_worker():
76
+ try:
77
+ conn = manager.get_connection("test_account")
78
+ results.append(conn)
79
+ except Exception as e:
80
+ results.append(e)
81
+
82
+ # Start multiple threads simultaneously
83
+ for _ in range(5):
84
+ thread = threading.Thread(target=get_connection_worker)
85
+ threads.append(thread)
86
+
87
+ # Start all threads at roughly the same time
88
+ for thread in threads:
89
+ thread.start()
90
+
91
+ # Wait for all threads to complete
92
+ for thread in threads:
93
+ thread.join(timeout=5.0)
94
+ assert not thread.is_alive(), "Thread took too long to complete"
95
+
96
+ # Verify results
97
+ assert len(results) == 5, "All threads should have completed"
98
+ assert all(
99
+ r is not None for r in results
100
+ ), "All threads should have gotten a connection"
101
+
102
+ # Most importantly: only one connection should have been created
103
+ assert len(manager.connections) == 1, "Only one connection should exist"
104
+ assert (
105
+ "test_account" in manager.connections
106
+ ), "Connection should be for test_account"
107
+
108
+ # Verify connect was called only once despite multiple concurrent requests
109
+ # (The exact number may vary due to timing, but should be minimal)
110
+ assert (
111
+ len(connection_attempts) <= 2
112
+ ), f"Too many connection attempts: {len(connection_attempts)}"
113
+
114
+ def test_concurrent_principal_access(self, mock_config_with_account):
115
+ """Test that concurrent principal access is thread-safe"""
116
+ with (
117
+ patch("chronos_mcp.accounts.DAVClient") as mock_dav_client,
118
+ patch("chronos_mcp.accounts.get_credential_manager") as mock_cred_mgr,
119
+ ):
120
+
121
+ # Setup mocks
122
+ mock_client = Mock()
123
+ mock_principal = Mock()
124
+ mock_client.principal.return_value = mock_principal
125
+ mock_dav_client.return_value = mock_client
126
+
127
+ mock_cred_mgr.return_value.get_password.return_value = "testpass"
128
+
129
+ manager = AccountManager(mock_config_with_account)
130
+
131
+ # Create multiple threads that try to get the same principal
132
+ threads = []
133
+ results = []
134
+
135
+ def get_principal_worker():
136
+ try:
137
+ principal = manager.get_principal("test_account")
138
+ results.append(principal)
139
+ except Exception as e:
140
+ results.append(e)
141
+
142
+ # Start multiple threads simultaneously
143
+ for _ in range(3):
144
+ thread = threading.Thread(target=get_principal_worker)
145
+ threads.append(thread)
146
+
147
+ for thread in threads:
148
+ thread.start()
149
+
150
+ for thread in threads:
151
+ thread.join(timeout=5.0)
152
+ assert not thread.is_alive(), "Thread took too long to complete"
153
+
154
+ # Verify results
155
+ assert len(results) == 3, "All threads should have completed"
156
+ assert all(
157
+ r is not None for r in results
158
+ ), "All threads should have gotten a principal"
159
+
160
+ # Only one principal should exist in cache
161
+ assert len(manager.principals) == 1, "Only one principal should exist"
162
+ assert (
163
+ "test_account" in manager.principals
164
+ ), "Principal should be for test_account"
165
+
166
+ def test_connection_lock_per_account(self, mock_config_with_account):
167
+ """Test that different accounts have different locks"""
168
+ with (
169
+ patch("chronos_mcp.accounts.DAVClient") as mock_dav_client,
170
+ patch("chronos_mcp.accounts.get_credential_manager") as mock_cred_mgr,
171
+ ):
172
+
173
+ # Setup mocks for multiple accounts
174
+ mock_client = Mock()
175
+ mock_principal = Mock()
176
+ mock_client.principal.return_value = mock_principal
177
+ mock_dav_client.return_value = mock_client
178
+
179
+ mock_cred_mgr.return_value.get_password.return_value = "testpass"
180
+
181
+ # Setup config to return different accounts
182
+ def get_account_side_effect(alias):
183
+ return Account(
184
+ alias=alias,
185
+ url=f"https://{alias}.example.com/",
186
+ username="testuser",
187
+ password="testpass",
188
+ display_name=f"Test Account {alias}",
189
+ )
190
+
191
+ mock_config_with_account.get_account.side_effect = get_account_side_effect
192
+
193
+ manager = AccountManager(mock_config_with_account)
194
+
195
+ # Access connections for different accounts concurrently
196
+ results = []
197
+
198
+ def get_connection_worker(account_alias):
199
+ try:
200
+ conn = manager.get_connection(account_alias)
201
+ results.append((account_alias, conn))
202
+ except Exception as e:
203
+ results.append((account_alias, e))
204
+
205
+ threads = []
206
+ account_aliases = ["account1", "account2", "account3"]
207
+
208
+ for alias in account_aliases:
209
+ thread = threading.Thread(target=get_connection_worker, args=(alias,))
210
+ threads.append(thread)
211
+
212
+ for thread in threads:
213
+ thread.start()
214
+
215
+ for thread in threads:
216
+ thread.join(timeout=5.0)
217
+
218
+ # Verify all accounts got connections
219
+ assert len(results) == 3, "All threads should have completed"
220
+ assert (
221
+ len(manager.connections) == 3
222
+ ), "Should have connections for all accounts"
223
+ assert (
224
+ len(manager._connection_locks) == 3
225
+ ), "Should have locks for all accounts"
226
+
227
+ # Verify different locks for different accounts
228
+ lock_ids = set()
229
+ for alias in account_aliases:
230
+ if alias in manager._connection_locks:
231
+ lock_ids.add(id(manager._connection_locks[alias]))
232
+
233
+ assert len(lock_ids) == 3, "Each account should have its own lock instance"
234
+
235
+ def test_error_handling_in_concurrent_access(self, mock_config_with_account):
236
+ """Test that errors in one thread don't affect others"""
237
+ with (
238
+ patch("chronos_mcp.accounts.DAVClient") as mock_dav_client,
239
+ patch("chronos_mcp.accounts.get_credential_manager") as mock_cred_mgr,
240
+ ):
241
+
242
+ # Setup mock to fail on first call, succeed on others
243
+ call_count = 0
244
+
245
+ def failing_connect(*args, **kwargs):
246
+ nonlocal call_count
247
+ call_count += 1
248
+ if call_count == 1:
249
+ raise Exception("First connection fails")
250
+
251
+ mock_client = Mock()
252
+ mock_principal = Mock()
253
+ mock_client.principal.return_value = mock_principal
254
+ return mock_client
255
+
256
+ mock_dav_client.side_effect = failing_connect
257
+ mock_cred_mgr.return_value.get_password.return_value = "testpass"
258
+
259
+ manager = AccountManager(mock_config_with_account)
260
+
261
+ results = []
262
+ errors = []
263
+
264
+ def get_connection_worker():
265
+ try:
266
+ conn = manager.get_connection("test_account")
267
+ results.append(conn)
268
+ except Exception as e:
269
+ errors.append(e)
270
+
271
+ # Start multiple threads
272
+ threads = []
273
+ for _ in range(3):
274
+ thread = threading.Thread(target=get_connection_worker)
275
+ threads.append(thread)
276
+ thread.start()
277
+
278
+ for thread in threads:
279
+ thread.join(timeout=5.0)
280
+
281
+ # One thread should have failed, others should succeed or get None
282
+ # (Exact behavior depends on timing and error handling)
283
+ total_attempts = len(results) + len(errors)
284
+ assert total_attempts == 3, "All threads should have completed"
285
+
286
+ def test_lock_cleanup_on_disconnect(self, mock_config_with_account):
287
+ """Test that locks are properly managed when connections are disconnected"""
288
+ manager = AccountManager(mock_config_with_account)
289
+
290
+ # Force creation of locks by accessing _connection_locks
291
+ manager._connection_locks["test_account"] = threading.Lock()
292
+ manager.connections["test_account"] = Mock()
293
+ manager.principals["test_account"] = Mock()
294
+
295
+ # Disconnect should clean up connections and principals
296
+ manager.disconnect_account("test_account")
297
+
298
+ assert "test_account" not in manager.connections
299
+ assert "test_account" not in manager.principals
300
+ # Note: We intentionally keep locks around to avoid lock creation overhead
301
+ # This is acceptable since locks are lightweight