forked from homeassistant-ai/ha-mcp
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_calendar.py
More file actions
498 lines (410 loc) · 18 KB
/
Copy pathtest_calendar.py
File metadata and controls
498 lines (410 loc) · 18 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
"""
Calendar Management E2E Tests
Tests the calendar event management tools:
- ha_config_get_calendar_events - Get events from a calendar
- ha_config_set_calendar_event - Create a calendar event
- ha_config_remove_calendar_event - Delete a calendar event
Note: These tests require calendar integrations to be configured in Home Assistant.
The tests are designed to work with the demo integration's calendar or local calendar.
Use ha_search_entities(query='calendar', domain_filter='calendar') to find calendar entities.
"""
import logging
import uuid
from datetime import datetime, timedelta
import pytest
from ...utilities.assertions import (
assert_mcp_success,
parse_mcp_result,
safe_call_tool,
)
logger = logging.getLogger(__name__)
@pytest.mark.calendar
class TestCalendarEvents:
"""Test calendar event retrieval functionality."""
async def _find_calendar_entity(self, mcp_client) -> str | None:
"""Find an available calendar entity for testing."""
result = await mcp_client.call_tool(
"ha_search_entities",
{"query": "calendar", "domain_filter": "calendar", "limit": 10},
)
data = parse_mcp_result(result)
# Handle nested data structure
if "data" in data:
results = data.get("data", {}).get("results", [])
else:
results = data.get("results", [])
if not results:
return None
# Return the first calendar found
return results[0].get("entity_id")
async def test_get_calendar_events_default_range(self, mcp_client):
"""
Test: Get calendar events with default time range
Retrieves events for the next 7 days (default behavior).
"""
calendar_entity = await self._find_calendar_entity(mcp_client)
if not calendar_entity:
pytest.skip("No calendar entities available for testing")
logger.info(f"Testing ha_config_get_calendar_events with {calendar_entity}...")
result = await mcp_client.call_tool(
"ha_config_get_calendar_events", {"entity_id": calendar_entity}
)
data = assert_mcp_success(result, "get calendar events")
# Validate response structure
assert "events" in data, "Response should contain 'events' key"
assert "count" in data, "Response should contain 'count' key"
assert "time_range" in data, "Response should contain 'time_range' key"
assert isinstance(data["events"], list), "Events should be a list"
logger.info(f"Retrieved {data['count']} event(s) from {calendar_entity}")
logger.info(f"Time range: {data['time_range']}")
# Validate event structure if events exist
for event in data["events"]:
logger.info(f" - Event: {event.get('summary', 'Untitled')}")
logger.info("ha_config_get_calendar_events default range test completed")
async def test_get_calendar_events_custom_range(self, mcp_client):
"""
Test: Get calendar events with custom time range
Retrieves events for a specific date range.
"""
calendar_entity = await self._find_calendar_entity(mcp_client)
if not calendar_entity:
pytest.skip("No calendar entities available for testing")
logger.info(
f"Testing ha_config_get_calendar_events with custom range for {calendar_entity}..."
)
# Set a custom time range (next 30 days)
now = datetime.now()
start = now.isoformat()
end = (now + timedelta(days=30)).isoformat()
result = await mcp_client.call_tool(
"ha_config_get_calendar_events",
{
"entity_id": calendar_entity,
"start": start,
"end": end,
"max_results": 5,
},
)
data = assert_mcp_success(result, "get calendar events with custom range")
# Validate response
assert "events" in data, "Response should contain 'events' key"
assert data["count"] <= 5, "Should respect max_results limit"
logger.info(
f"Retrieved {data['count']} event(s) with max_results=5, total_available={data.get('total_available', 'unknown')}"
)
logger.info("ha_config_get_calendar_events custom range test completed")
async def test_get_calendar_events_invalid_entity(self, mcp_client):
"""
Test: Get events from invalid calendar entity
Verifies proper error handling for non-existent calendars.
"""
logger.info("Testing ha_config_get_calendar_events with invalid entity...")
# Use safe_call_tool since we expect this to fail
data = await safe_call_tool(
mcp_client,
"ha_config_get_calendar_events",
{"entity_id": "calendar.nonexistent_calendar_xyz"},
)
# Should fail gracefully
assert data.get("success") is False, "Should fail for invalid calendar"
assert "error" in data or "suggestions" in data, "Should provide error info"
logger.info(f"Error (expected): {data.get('error', 'Unknown')}")
logger.info("Invalid entity test completed")
async def test_get_calendar_events_invalid_entity_format(self, mcp_client):
"""
Test: Get events with invalid entity format
Verifies validation of entity_id format.
"""
logger.info(
"Testing ha_config_get_calendar_events with invalid entity format..."
)
# Use safe_call_tool since we expect this to fail
data = await safe_call_tool(
mcp_client,
"ha_config_get_calendar_events",
{"entity_id": "not_a_calendar_entity"},
)
# Should fail with validation error
assert data.get("success") is False, "Should fail for invalid format"
assert "calendar." in str(data.get("error", "")), (
"Error should mention correct format"
)
logger.info(f"Validation error (expected): {data.get('error', 'Unknown')}")
logger.info("Invalid format test completed")
@pytest.mark.calendar
@pytest.mark.slow
class TestCalendarEventLifecycle:
"""Test calendar event creation and deletion lifecycle."""
async def _find_writable_calendar(self, mcp_client) -> str | None:
"""Find a calendar that supports event creation."""
result = await mcp_client.call_tool(
"ha_search_entities",
{"query": "calendar", "domain_filter": "calendar", "limit": 10},
)
data = parse_mcp_result(result)
# Handle nested data structure
if "data" in data:
results = data.get("data", {}).get("results", [])
else:
results = data.get("results", [])
if not results:
return None
# Prefer local calendar if available (usually writable)
for cal in results:
entity_id = cal.get("entity_id", "")
if "local" in entity_id.lower():
return entity_id
# Fall back to first calendar
return results[0].get("entity_id")
async def test_create_calendar_event(self, mcp_client, cleanup_tracker):
"""
Test: Create a calendar event
Creates a test event and verifies it was created successfully.
"""
calendar_entity = await self._find_writable_calendar(mcp_client)
if not calendar_entity:
pytest.skip("No calendar entities available for testing")
logger.info(f"Testing ha_config_set_calendar_event in {calendar_entity}...")
# Create an event for tomorrow
now = datetime.now()
start = (now + timedelta(days=1)).replace(
hour=14, minute=0, second=0, microsecond=0
)
end = start + timedelta(hours=1)
event_summary = "E2E Test Event - Safe to Delete"
try:
result = await mcp_client.call_tool(
"ha_config_set_calendar_event",
{
"entity_id": calendar_entity,
"summary": event_summary,
"start": start.isoformat(),
"end": end.isoformat(),
"description": "This is a test event created by E2E tests",
"location": "Test Location",
},
)
data = parse_mcp_result(result)
if data.get("success"):
logger.info(f"Event created successfully: {event_summary}")
logger.info(f"Event details: {data.get('event', {})}")
# Track for potential cleanup
cleanup_tracker.track(
"calendar_event", f"{calendar_entity}:{event_summary}"
)
# Verify event appears in calendar
events_result = await mcp_client.call_tool(
"ha_config_get_calendar_events",
{
"entity_id": calendar_entity,
"start": start.isoformat(),
"end": (end + timedelta(hours=1)).isoformat(),
},
)
events_data = parse_mcp_result(events_result)
logger.info(
f"Events after creation: {events_data.get('count', 0)} event(s)"
)
else:
# Calendar might not support event creation
error_msg = data.get("error", "Unknown error")
if "not supported" in error_msg.lower() or "read" in error_msg.lower():
pytest.skip(
f"Calendar {calendar_entity} does not support event creation"
)
else:
logger.warning(f"Event creation failed: {error_msg}")
# Don't fail the test - some calendars are read-only
pytest.skip(f"Calendar event creation not available: {error_msg}")
except Exception as e:
logger.warning(f"Event creation test encountered error: {e}")
pytest.skip(f"Calendar event creation not available: {e}")
logger.info("ha_config_set_calendar_event test completed")
async def test_create_calendar_event_invalid_entity(self, mcp_client):
"""
Test: Create event with invalid calendar entity
Verifies proper error handling for invalid entity.
"""
logger.info("Testing ha_config_set_calendar_event with invalid entity...")
now = datetime.now()
start = (now + timedelta(days=1)).isoformat()
end = (now + timedelta(days=1, hours=1)).isoformat()
# Use safe_call_tool since we expect this to fail
data = await safe_call_tool(
mcp_client,
"ha_config_set_calendar_event",
{
"entity_id": "not_a_valid_calendar",
"summary": "Test Event",
"start": start,
"end": end,
},
)
assert data.get("success") is False, "Should fail for invalid entity"
assert "calendar." in str(data.get("error", "")), (
"Error should mention correct format"
)
logger.info(f"Validation error (expected): {data.get('error', 'Unknown')}")
logger.info("Invalid entity create test completed")
@pytest.fixture
async def deletable_event_uid(self, mcp_client):
"""Create a temporary event, yield (entity_id, uid), then best-effort delete.
``ha_config_set_calendar_event`` does not return the assigned UID, so
the fixture round-trips through ``ha_config_get_calendar_events`` to
retrieve it. Teardown swallows exceptions to stay idempotent regardless
of whether the test body already deleted the event.
"""
calendar_entity = await self._find_writable_calendar(mcp_client)
if not calendar_entity:
pytest.skip("No writable calendar found for testing")
summary = f"E2E Deletable Test Event {uuid.uuid4().hex[:8]}"
now = datetime.now()
start = (now + timedelta(days=1)).replace(
hour=14, minute=0, second=0, microsecond=0
)
end = start + timedelta(hours=1)
create_data = await safe_call_tool(
mcp_client,
"ha_config_set_calendar_event",
{
"entity_id": calendar_entity,
"summary": summary,
"start": start.isoformat(),
"end": end.isoformat(),
},
)
if not create_data.get("success"):
pytest.skip(
f"Calendar {calendar_entity} does not support event creation: "
f"{create_data.get('error', 'Unknown')}"
)
events_data = await safe_call_tool(
mcp_client,
"ha_config_get_calendar_events",
{
"entity_id": calendar_entity,
"start": start.isoformat(),
"end": (end + timedelta(hours=1)).isoformat(),
},
)
event_uid = next(
(
e.get("uid")
for e in events_data.get("events", [])
if e.get("summary") == summary
),
None,
)
if not event_uid:
pytest.fail(
f"Created event '{summary}' did not surface a uid in "
f"ha_config_get_calendar_events for {calendar_entity}"
)
try:
yield calendar_entity, event_uid
finally:
try:
await mcp_client.call_tool(
"ha_config_remove_calendar_event",
{"entity_id": calendar_entity, "uid": event_uid},
)
except Exception as cleanup_error:
logger.debug(
f"Cleanup of test event {event_uid} on {calendar_entity}: "
f"{cleanup_error}"
)
async def test_delete_calendar_event(self, mcp_client, deletable_event_uid):
"""
Test: Delete a calendar event (positive + negative paths)
Creates a fresh event, deletes it (positive: hard-assert success), then
re-attempts deletion of the released UID (negative: hard-assert failure
with suggestions). UID-collision risk is eliminated because the UID was
just held and released by this test.
"""
calendar_entity, event_uid = deletable_event_uid
logger.info(
f"Testing ha_config_remove_calendar_event for {calendar_entity} "
f"with uid={event_uid}..."
)
first_delete = await mcp_client.call_tool(
"ha_config_remove_calendar_event",
{"entity_id": calendar_entity, "uid": event_uid},
)
assert_mcp_success(first_delete, "first deletion of just-created event")
logger.info(f"Deleted event {event_uid} (positive path)")
second_delete = await safe_call_tool(
mcp_client,
"ha_config_remove_calendar_event",
{"entity_id": calendar_entity, "uid": event_uid},
)
assert second_delete.get("success") is False, (
f"Second deletion of released UID should fail: got {second_delete}"
)
assert second_delete.get("error", {}).get("suggestions"), (
"Delete failure should provide helpful suggestions"
)
logger.info(
f"Second delete failed as expected: {second_delete.get('error', 'Unknown')}"
)
logger.info("ha_config_remove_calendar_event test completed")
async def test_delete_calendar_event_invalid_entity(self, mcp_client):
"""
Test: Delete event with invalid calendar entity
Verifies proper error handling for invalid entity format.
"""
logger.info("Testing ha_config_remove_calendar_event with invalid entity...")
# Use safe_call_tool since we expect this to fail
data = await safe_call_tool(
mcp_client,
"ha_config_remove_calendar_event",
{"entity_id": "not_a_valid_calendar", "uid": "some-event-uid"},
)
assert data.get("success") is False, "Should fail for invalid entity"
assert "calendar." in str(data.get("error", "")), (
"Error should mention correct format"
)
logger.info(f"Validation error (expected): {data.get('error', 'Unknown')}")
logger.info("Invalid entity delete test completed")
@pytest.mark.calendar
async def test_calendar_tools_overview(mcp_client):
"""
Test: Verify calendar tools are registered and accessible
This test validates that all calendar tools are properly
registered with the MCP server.
"""
logger.info("Verifying calendar tools registration...")
# Test get events tool registration (even if it fails due to invalid entity)
get_data = await safe_call_tool(
mcp_client, "ha_config_get_calendar_events", {"entity_id": "calendar.test"}
)
assert "events" in get_data or "error" in get_data, (
"ha_config_get_calendar_events should return events or error"
)
logger.info("ha_config_get_calendar_events tool is registered and functional")
# Test create event tool registration
now = datetime.now()
create_data = await safe_call_tool(
mcp_client,
"ha_config_set_calendar_event",
{
"entity_id": "calendar.test",
"summary": "Test",
"start": now.isoformat(),
"end": (now + timedelta(hours=1)).isoformat(),
},
)
assert "event" in create_data or "error" in create_data, (
"ha_config_set_calendar_event should return event or error"
)
logger.info("ha_config_set_calendar_event tool is registered and functional")
# Test delete event tool registration
delete_data = await safe_call_tool(
mcp_client,
"ha_config_remove_calendar_event",
{"entity_id": "calendar.test", "uid": "test-uid"},
)
assert "uid" in delete_data or "error" in delete_data, (
"ha_config_remove_calendar_event should return uid or error"
)
logger.info("ha_config_remove_calendar_event tool is registered and functional")
logger.info("All calendar tools are properly registered")