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,372 @@
1
+ """
2
+ Unit tests for event search functionality
3
+ """
4
+
5
+ import re
6
+ from datetime import datetime, timedelta
7
+
8
+ import pytest
9
+
10
+ from chronos_mcp.search import SearchOptions, calculate_relevance_score
11
+ from chronos_mcp.search import search_events as search_events_func
12
+ from chronos_mcp.search import search_events_ranked
13
+
14
+
15
+ class TestSearchOptions:
16
+ def test_search_options_defaults(self):
17
+ """Test SearchOptions with default values"""
18
+ opts = SearchOptions(
19
+ query="meeting", fields=["summary", "description", "location"]
20
+ )
21
+
22
+ assert opts.query == "meeting"
23
+ assert opts.fields == ["summary", "description", "location"]
24
+ assert opts.case_sensitive is False
25
+ assert opts.match_type == "contains"
26
+ assert opts.use_regex is False
27
+ assert opts.date_start is None
28
+ assert opts.date_end is None
29
+ assert opts.max_results is None
30
+
31
+ def test_search_options_validation(self):
32
+ """Test SearchOptions validation"""
33
+ # Invalid match type
34
+ with pytest.raises(ValueError) as exc_info:
35
+ SearchOptions(
36
+ query="test",
37
+ fields=["summary", "description", "location"],
38
+ match_type="invalid",
39
+ )
40
+ assert "match_type must be one of" in str(exc_info.value)
41
+
42
+ # Valid match types
43
+ for match_type in ["contains", "starts_with", "ends_with", "exact", "regex"]:
44
+ opts = SearchOptions(
45
+ query="test",
46
+ fields=["summary", "description", "location"],
47
+ match_type=match_type,
48
+ )
49
+ assert opts.match_type == match_type
50
+
51
+ def test_regex_pattern_compilation(self):
52
+ """Test regex pattern compilation"""
53
+ # Valid regex
54
+ opts = SearchOptions(
55
+ query=r"Meeting.*\d+",
56
+ fields=["summary", "description", "location"],
57
+ use_regex=True,
58
+ )
59
+ assert hasattr(opts, "pattern")
60
+ assert opts.pattern.search("Meeting 123")
61
+
62
+ # Invalid regex should raise re.error
63
+ with pytest.raises(re.error):
64
+ SearchOptions(
65
+ query=r"[invalid(",
66
+ fields=["summary", "description", "location"],
67
+ use_regex=True,
68
+ )
69
+
70
+
71
+ class TestSearchEvents:
72
+ def create_test_events(self):
73
+ """Create test event data"""
74
+ base_date = datetime.now()
75
+ return [
76
+ {
77
+ "uid": "1",
78
+ "summary": "Team Meeting",
79
+ "description": "Weekly team sync",
80
+ "location": "Conference Room A",
81
+ "dtstart": base_date - timedelta(days=1),
82
+ },
83
+ {
84
+ "uid": "2",
85
+ "summary": "Project Review",
86
+ "description": "Review project status with team",
87
+ "location": "Zoom",
88
+ "dtstart": base_date + timedelta(days=1),
89
+ },
90
+ {
91
+ "uid": "3",
92
+ "summary": "Client Call",
93
+ "description": "Discuss requirements",
94
+ "location": "Meeting Room B",
95
+ "dtstart": base_date + timedelta(days=3),
96
+ },
97
+ {
98
+ "uid": "4",
99
+ "summary": "Lunch Break",
100
+ "description": "Team lunch outing",
101
+ "location": "Cafeteria",
102
+ "dtstart": base_date,
103
+ },
104
+ ]
105
+
106
+ def test_basic_contains_search(self):
107
+ """Test basic contains search"""
108
+ events = self.create_test_events()
109
+ opts = SearchOptions(
110
+ query="team",
111
+ fields=["summary", "description", "location"],
112
+ case_sensitive=False,
113
+ )
114
+
115
+ results = search_events_func(events, opts)
116
+ assert len(results) == 3 # Team Meeting, team sync, Team lunch
117
+
118
+ # Case sensitive
119
+ opts_case = SearchOptions(
120
+ query="team",
121
+ fields=["summary", "description", "location"],
122
+ case_sensitive=True,
123
+ )
124
+ results_case = search_events_func(events, opts_case)
125
+ assert len(results_case) == 2 # Only lowercase 'team' matches
126
+
127
+ def test_field_specific_search(self):
128
+ """Test searching specific fields"""
129
+ events = self.create_test_events()
130
+
131
+ # Search only in summary
132
+ opts = SearchOptions(query="team", fields=["summary"])
133
+ results = search_events_func(events, opts)
134
+ assert len(results) == 1 # Only "Team Meeting"
135
+
136
+ # Search only in location
137
+ opts = SearchOptions(query="room", fields=["location"])
138
+ results = search_events_func(events, opts)
139
+ assert len(results) == 2 # Conference Room A, Meeting Room B
140
+
141
+ def test_match_type_search(self):
142
+ """Test different match types"""
143
+ events = self.create_test_events()
144
+
145
+ # Starts with
146
+ opts = SearchOptions(
147
+ query="team",
148
+ fields=["summary", "description", "location"],
149
+ match_type="starts_with",
150
+ )
151
+ results = search_events_func(events, opts)
152
+ assert len(results) == 2 # Team Meeting, Team lunch
153
+
154
+ # Ends with
155
+ opts = SearchOptions(
156
+ query="call",
157
+ fields=["summary", "description", "location"],
158
+ match_type="ends_with",
159
+ )
160
+ results = search_events_func(events, opts)
161
+ assert len(results) == 1 # Client Call
162
+
163
+ # Exact match
164
+ opts = SearchOptions(
165
+ query="Lunch Break",
166
+ fields=["summary", "description", "location"],
167
+ match_type="exact",
168
+ )
169
+ results = search_events_func(events, opts)
170
+ assert len(results) == 1
171
+ assert results[0]["uid"] == "4"
172
+
173
+ def test_date_range_search(self):
174
+ """Test date range filtering"""
175
+ events = self.create_test_events()
176
+ base_date = datetime.now()
177
+
178
+ # Future events only (including today)
179
+ opts = SearchOptions(
180
+ query="", # No text filter
181
+ fields=["summary", "description", "location"],
182
+ date_start=base_date
183
+ - timedelta(seconds=1), # Slightly before to include "today"
184
+ date_end=base_date + timedelta(days=7),
185
+ )
186
+ results = search_events_func(events, opts)
187
+ assert len(results) == 3 # Today's lunch + 2 future events
188
+
189
+ # Past events only
190
+ opts = SearchOptions(
191
+ query="",
192
+ fields=["summary", "description", "location"],
193
+ date_start=base_date - timedelta(days=7),
194
+ date_end=base_date - timedelta(hours=1),
195
+ )
196
+ results = search_events_func(events, opts)
197
+ assert len(results) == 1 # Yesterday's team meeting
198
+
199
+ def test_combined_text_and_date_search(self):
200
+ """Test combining text and date filters"""
201
+ events = self.create_test_events()
202
+ base_date = datetime.now()
203
+
204
+ opts = SearchOptions(
205
+ query="meeting",
206
+ fields=["summary", "description", "location"],
207
+ date_start=base_date,
208
+ date_end=base_date + timedelta(days=7),
209
+ )
210
+ results = search_events_func(events, opts)
211
+ assert len(results) == 1 # Only future meeting (Meeting Room B)
212
+ assert results[0]["uid"] == "3"
213
+
214
+ def test_regex_search(self):
215
+ """Test regex pattern search"""
216
+ events = self.create_test_events()
217
+
218
+ # Match "Room" followed by a letter
219
+ opts = SearchOptions(
220
+ query=r"Room\s+[A-Z]",
221
+ fields=["summary", "description", "location"],
222
+ use_regex=True,
223
+ )
224
+ results = search_events_func(events, opts)
225
+ assert len(results) == 2 # Conference Room A, Meeting Room B
226
+
227
+ # Match events with numbers
228
+ events[0]["summary"] = "Team Meeting #123"
229
+ opts = SearchOptions(
230
+ query=r"#\d+", fields=["summary", "description", "location"], use_regex=True
231
+ )
232
+ results = search_events_func(events, opts)
233
+ assert len(results) == 1
234
+ assert results[0]["uid"] == "1"
235
+
236
+ def test_max_results_limiting(self):
237
+ """Test result limiting"""
238
+ events = self.create_test_events()
239
+
240
+ opts = SearchOptions(
241
+ query="e", fields=["summary", "description", "location"], max_results=2
242
+ ) # Matches all events
243
+ results = search_events_func(events, opts)
244
+ assert len(results) == 2 # Limited to 2 results
245
+
246
+
247
+ class TestRelevanceScoring:
248
+ def test_field_weight_scoring(self):
249
+ """Test that different fields have different weights"""
250
+ opts = SearchOptions(
251
+ query="important", fields=["summary", "description", "location"]
252
+ )
253
+
254
+ # Event with match in summary (weight 3.0)
255
+ event_summary = {
256
+ "summary": "Important Meeting",
257
+ "description": "Regular meeting",
258
+ "location": "Room A",
259
+ }
260
+
261
+ # Event with match in description (weight 2.0)
262
+ event_desc = {
263
+ "summary": "Regular Meeting",
264
+ "description": "Important topics to discuss",
265
+ "location": "Room B",
266
+ }
267
+
268
+ # Event with match in location (weight 1.0)
269
+ event_loc = {
270
+ "summary": "Regular Meeting",
271
+ "description": "Weekly sync",
272
+ "location": "Important Building",
273
+ }
274
+
275
+ score_summary = calculate_relevance_score(event_summary, opts)
276
+ score_desc = calculate_relevance_score(event_desc, opts)
277
+ score_loc = calculate_relevance_score(event_loc, opts)
278
+
279
+ assert score_summary > score_desc > score_loc
280
+
281
+ def test_position_scoring(self):
282
+ """Test that earlier matches score higher"""
283
+ opts = SearchOptions(
284
+ query="meeting", fields=["summary", "description", "location"]
285
+ )
286
+
287
+ # Match at beginning
288
+ event_start = {
289
+ "summary": "Meeting with client",
290
+ "description": "",
291
+ "location": "",
292
+ }
293
+
294
+ # Match at end
295
+ event_end = {
296
+ "summary": "Client discussion meeting",
297
+ "description": "",
298
+ "location": "",
299
+ }
300
+
301
+ score_start = calculate_relevance_score(event_start, opts)
302
+ score_end = calculate_relevance_score(event_end, opts)
303
+
304
+ assert score_start > score_end
305
+
306
+ def test_recency_scoring(self):
307
+ """Test that recent events get a boost"""
308
+ opts = SearchOptions(
309
+ query="meeting", fields=["summary", "description", "location"]
310
+ )
311
+ current_time = datetime.now()
312
+
313
+ # Event from today
314
+ event_today = {"summary": "Team Meeting", "dtstart": current_time}
315
+
316
+ # Event from 20 days ago
317
+ event_old = {
318
+ "summary": "Team Meeting",
319
+ "dtstart": current_time - timedelta(days=20),
320
+ }
321
+
322
+ score_today = calculate_relevance_score(event_today, opts, current_time)
323
+ score_old = calculate_relevance_score(event_old, opts, current_time)
324
+
325
+ assert score_today > score_old
326
+
327
+ def test_search_events_ranked(self):
328
+ """Test ranked search results"""
329
+ events = [
330
+ {
331
+ "uid": "1",
332
+ "summary": "Important Team Meeting",
333
+ "description": "",
334
+ "location": "",
335
+ },
336
+ {
337
+ "uid": "2",
338
+ "summary": "Meeting",
339
+ "description": "Important topics",
340
+ "location": "",
341
+ },
342
+ {
343
+ "uid": "3",
344
+ "summary": "Lunch",
345
+ "description": "",
346
+ "location": "Meeting Room",
347
+ },
348
+ {
349
+ "uid": "4",
350
+ "summary": "Team Meeting Tomorrow",
351
+ "description": "",
352
+ "location": "",
353
+ },
354
+ ]
355
+
356
+ opts = SearchOptions(
357
+ query="meeting", fields=["summary", "description", "location"]
358
+ )
359
+ ranked_results = search_events_ranked(events, opts)
360
+
361
+ assert len(ranked_results) == 4
362
+ assert all(isinstance(r[1], float) for r in ranked_results) # All have scores
363
+
364
+ # First result should have highest score
365
+ assert ranked_results[0][1] > ranked_results[-1][1]
366
+
367
+ # Check that it found the right events
368
+ uids = [r[0]["uid"] for r in ranked_results]
369
+ assert "1" in uids # Has "Meeting" in summary
370
+ assert "2" in uids # Has "Meeting" in summary
371
+ assert "3" in uids # Has "Meeting" in location
372
+ assert "4" in uids # Has "Meeting" in summary
@@ -0,0 +1,333 @@
1
+ """
2
+ Unit tests for advanced search functionality
3
+ """
4
+
5
+ from datetime import datetime, timedelta
6
+ from unittest.mock import Mock, patch
7
+
8
+ import pytest
9
+
10
+ from chronos_mcp.models import Event
11
+
12
+ # Import the actual function directly
13
+ from chronos_mcp.server import search_events
14
+
15
+
16
+ class TestSearchEvents:
17
+ """Test the search_events function"""
18
+
19
+ @pytest.fixture
20
+ def mock_managers(self):
21
+ """Setup mock managers"""
22
+ from chronos_mcp.tools.events import _managers
23
+
24
+ # Save original state
25
+ original_managers = _managers.copy()
26
+
27
+ # Create mock managers
28
+ mock_calendar = Mock()
29
+ mock_event = Mock()
30
+ mock_logger = Mock()
31
+
32
+ # Set up the global _managers dict
33
+ _managers.clear()
34
+ _managers.update(
35
+ {
36
+ "calendar_manager": mock_calendar,
37
+ "event_manager": mock_event,
38
+ "logger": mock_logger,
39
+ }
40
+ )
41
+
42
+ try:
43
+ yield {
44
+ "calendar": mock_calendar,
45
+ "event": mock_event,
46
+ "logger": mock_logger,
47
+ }
48
+ finally:
49
+ # Restore original state
50
+ _managers.clear()
51
+ _managers.update(original_managers)
52
+
53
+ @pytest.fixture
54
+ def sample_events(self):
55
+ """Create sample events for testing"""
56
+ base_date = datetime.now()
57
+ return [
58
+ Event(
59
+ uid="evt-1",
60
+ summary="Team Meeting - Project Review",
61
+ description="Quarterly review meeting with the team",
62
+ start=base_date + timedelta(days=1),
63
+ end=base_date + timedelta(days=1, hours=1),
64
+ location="Conference Room A",
65
+ all_day=False,
66
+ calendar_uid="test-calendar",
67
+ account_alias="default",
68
+ ),
69
+ Event(
70
+ uid="evt-2",
71
+ summary="Client Call - ABC Corp",
72
+ description="Discuss contract renewal",
73
+ start=base_date + timedelta(days=2),
74
+ end=base_date + timedelta(days=2, hours=1),
75
+ location="Zoom Meeting",
76
+ all_day=False,
77
+ calendar_uid="test-calendar",
78
+ account_alias="default",
79
+ ),
80
+ Event(
81
+ uid="evt-3",
82
+ summary="Workshop: Python Best Practices",
83
+ description="Internal training workshop",
84
+ start=base_date + timedelta(days=3),
85
+ end=base_date + timedelta(days=3, hours=3),
86
+ location="Training Room",
87
+ all_day=False,
88
+ calendar_uid="test-calendar",
89
+ account_alias="default",
90
+ ),
91
+ ]
92
+
93
+ @pytest.mark.asyncio
94
+ async def test_search_events_basic(self, mock_managers, sample_events):
95
+ """Test basic search functionality"""
96
+ # Setup mocks
97
+ mock_cal = Mock()
98
+ mock_cal.uid = "test-calendar"
99
+ mock_managers["calendar"].list_calendars.return_value = [mock_cal]
100
+ mock_managers["event"].get_events_range.return_value = sample_events
101
+
102
+ # Execute search
103
+ result = await search_events.fn(
104
+ query="meeting",
105
+ fields=["summary", "description", "location"],
106
+ case_sensitive=False,
107
+ date_start=None,
108
+ date_end=None,
109
+ calendar_uid=None,
110
+ max_results=50,
111
+ account=None,
112
+ )
113
+
114
+ # Debug print
115
+ if not result["success"]:
116
+ print(f"Search failed: {result}")
117
+
118
+ # Verify results
119
+ assert result["success"] is True
120
+ assert result["query"] == "meeting"
121
+ assert len(result["matches"]) == 2 # Should find 2 events with "meeting"
122
+ assert result["total"] == 2
123
+ assert result["truncated"] is False
124
+
125
+ # Check matched events
126
+ matches = result["matches"]
127
+ found_uids = {match["uid"] for match in matches}
128
+ assert "evt-1" in found_uids # Has "meeting" in summary
129
+ assert "evt-2" in found_uids # Has "Meeting" in location (Zoom Meeting)
130
+
131
+ @pytest.mark.asyncio
132
+ async def test_search_events_case_sensitive(self, mock_managers, sample_events):
133
+ """Test case-sensitive search"""
134
+ mock_cal = Mock()
135
+ mock_cal.uid = "test-calendar"
136
+ mock_managers["calendar"].list_calendars.return_value = [mock_cal]
137
+ mock_managers["event"].get_events_range.return_value = sample_events
138
+
139
+ # Case-sensitive search
140
+ # Direct function call
141
+ result = await search_events.fn(
142
+ query="Meeting",
143
+ fields=["summary", "description", "location"],
144
+ case_sensitive=True,
145
+ date_start=None,
146
+ date_end=None,
147
+ calendar_uid=None,
148
+ max_results=50,
149
+ account=None,
150
+ )
151
+
152
+ assert result["success"] is True
153
+ assert (
154
+ len(result["matches"]) == 2
155
+ ) # Both "Team Meeting" and "Zoom Meeting" match
156
+ # Check that both events with "Meeting" are found
157
+ found_uids = {match["uid"] for match in result["matches"]}
158
+ assert "evt-1" in found_uids # "Team Meeting - Project Review"
159
+ assert "evt-2" in found_uids # "Zoom Meeting"
160
+
161
+ @pytest.mark.asyncio
162
+ async def test_search_events_specific_calendar(self, mock_managers, sample_events):
163
+ """Test searching specific calendar"""
164
+ mock_managers["event"].get_events_range.return_value = sample_events
165
+
166
+ # Direct function call
167
+ result = await search_events.fn(
168
+ query="workshop",
169
+ fields=["summary", "description", "location"],
170
+ case_sensitive=False,
171
+ date_start=None,
172
+ date_end=None,
173
+ calendar_uid="specific-cal",
174
+ max_results=50,
175
+ account=None,
176
+ )
177
+
178
+ # Should search only the specified calendar
179
+ mock_managers["event"].get_events_range.assert_called_once()
180
+ call_args = mock_managers["event"].get_events_range.call_args[1]
181
+ assert call_args["calendar_uid"] == "specific-cal"
182
+
183
+ assert result["success"] is True
184
+ assert len(result["matches"]) == 1
185
+ assert result["matches"][0]["uid"] == "evt-3"
186
+
187
+ @pytest.mark.asyncio
188
+ async def test_search_events_validation_errors(self, mock_managers):
189
+ """Test input validation"""
190
+ # Query too short
191
+ # Direct function call
192
+ result = await search_events.fn(
193
+ query="a",
194
+ fields=["summary", "description", "location"],
195
+ case_sensitive=False,
196
+ date_start=None,
197
+ date_end=None,
198
+ calendar_uid=None,
199
+ max_results=50,
200
+ account=None,
201
+ )
202
+ assert result["success"] is False
203
+ assert "too short" in result["error"]
204
+
205
+ # Query too long
206
+ # Direct function call
207
+ result = await search_events.fn(
208
+ query="x" * 1001,
209
+ fields=["summary", "description", "location"],
210
+ case_sensitive=False,
211
+ date_start=None,
212
+ date_end=None,
213
+ calendar_uid=None,
214
+ max_results=50,
215
+ account=None,
216
+ )
217
+ assert result["success"] is False
218
+ assert "too long" in result["error"]
219
+
220
+ @pytest.mark.asyncio
221
+ async def test_search_events_field_validation(self, mock_managers):
222
+ """Test field validation"""
223
+ # Direct function call
224
+ result = await search_events.fn(
225
+ query="test",
226
+ fields=["summary", "__proto__", "description"],
227
+ case_sensitive=False,
228
+ date_start=None,
229
+ date_end=None,
230
+ calendar_uid=None,
231
+ max_results=50,
232
+ account=None,
233
+ )
234
+ assert result["success"] is False
235
+ assert "Invalid field" in result["error"]
236
+
237
+ @pytest.mark.asyncio
238
+ async def test_search_events_date_range(self, mock_managers, sample_events):
239
+ """Test date range filtering"""
240
+ mock_cal = Mock()
241
+ mock_cal.uid = "test-calendar"
242
+ mock_managers["calendar"].list_calendars.return_value = [mock_cal]
243
+ mock_managers["event"].get_events_range.return_value = sample_events
244
+
245
+ start_date = datetime.now().isoformat()
246
+ end_date = (datetime.now() + timedelta(days=30)).isoformat()
247
+
248
+ # Direct function call
249
+ result = await search_events.fn(
250
+ query="meeting",
251
+ fields=["summary", "description", "location"],
252
+ case_sensitive=False,
253
+ date_start=start_date,
254
+ date_end=end_date,
255
+ calendar_uid=None,
256
+ max_results=50,
257
+ account=None,
258
+ )
259
+
260
+ # Verify date parsing was called if search succeeded
261
+ if mock_managers["event"].get_events_range.call_args:
262
+ call_args = mock_managers["event"].get_events_range.call_args[1]
263
+ assert isinstance(call_args["start_date"], datetime)
264
+ assert isinstance(call_args["end_date"], datetime)
265
+
266
+ assert result["success"] is True
267
+
268
+ @pytest.mark.asyncio
269
+ async def test_search_events_max_results(self, mock_managers):
270
+ """Test max_results limiting"""
271
+ # Create many events
272
+ many_events = []
273
+ for i in range(20):
274
+ event = Event(
275
+ uid=f"evt-{i}",
276
+ summary=f"Meeting {i}",
277
+ description="Test meeting",
278
+ start=datetime.now() + timedelta(days=i),
279
+ end=datetime.now() + timedelta(days=i, hours=1),
280
+ all_day=False,
281
+ calendar_uid="test-calendar",
282
+ account_alias="default",
283
+ )
284
+ many_events.append(event)
285
+
286
+ mock_cal = Mock()
287
+ mock_cal.uid = "test-calendar"
288
+ mock_managers["calendar"].list_calendars.return_value = [mock_cal]
289
+ mock_managers["event"].get_events_range.return_value = many_events
290
+
291
+ # Direct function call
292
+ result = await search_events.fn(
293
+ query="meeting",
294
+ fields=["summary", "description", "location"],
295
+ case_sensitive=False,
296
+ date_start=None,
297
+ date_end=None,
298
+ calendar_uid=None,
299
+ max_results=5,
300
+ account=None,
301
+ )
302
+
303
+ assert result["success"] is True
304
+ assert len(result["matches"]) == 5
305
+ assert result["total"] == 5
306
+ assert result["truncated"] is False # We stop searching at max_results
307
+
308
+ @pytest.mark.asyncio
309
+ async def test_search_events_error_handling(self, mock_managers):
310
+ """Test error handling in search"""
311
+ mock_cal = Mock()
312
+ mock_cal.uid = "test-calendar"
313
+ mock_managers["calendar"].list_calendars.return_value = [mock_cal]
314
+ mock_managers["event"].get_events_range.side_effect = Exception(
315
+ "Calendar error"
316
+ )
317
+
318
+ # Direct function call
319
+ result = await search_events.fn(
320
+ query="meeting",
321
+ fields=["summary", "description", "location"],
322
+ case_sensitive=False,
323
+ date_start=None,
324
+ date_end=None,
325
+ calendar_uid=None,
326
+ max_results=50,
327
+ account=None,
328
+ )
329
+
330
+ # Should continue searching other calendars
331
+ assert result["success"] is True
332
+ assert result["total"] == 0
333
+ assert len(result["matches"]) == 0