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