Skip to content
Closed
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
174 changes: 133 additions & 41 deletions tests/src/e2e/workflows/calendar/test_calendar.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
"""

import logging
import uuid
from datetime import datetime, timedelta

import pytest
Expand All @@ -21,6 +22,7 @@
parse_mcp_result,
safe_call_tool,
)
from ...utilities.wait_helpers import wait_for_tool_result

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -59,9 +61,7 @@ async def test_get_calendar_events_default_range(self, 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}..."
)
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}
Expand Down Expand Up @@ -165,9 +165,9 @@ async def test_get_calendar_events_invalid_entity_format(self, mcp_client):

# 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"
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")
Expand Down Expand Up @@ -204,6 +204,76 @@ async def _find_writable_calendar(self, mcp_client) -> str | None:
# Fall back to first calendar
return results[0].get("entity_id")

@pytest.fixture
async def deletable_event_uid(self, mcp_client):
"""Create a temporary event, yield (entity_id, uid), then best-effort delete.

Round-trips through ha_config_get_calendar_events to obtain the UID that
HA assigned — ha_config_set_calendar_event does not return it in the
response. Teardown swallows exceptions so it stays 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")

unique_id = uuid.uuid4().hex[:8]
summary = f"E2E Deletable Test Event {unique_id}"
now = datetime.now()
Comment thread
Patch76 marked this conversation as resolved.
Outdated
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"):
error_msg = str(create_data.get("error", "Unknown"))
Comment thread
Patch76 marked this conversation as resolved.
pytest.skip(
f"Calendar {calendar_entity} does not support event creation: {error_msg}"
)

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(),
},
)
events = events_data.get("events", [])
event_uid = next(
(e.get("uid") for e in events if e.get("summary") == summary), None
)
if not event_uid:
pytest.skip(
f"Could not retrieve UID for created event '{summary}' from {calendar_entity}"
)
Comment thread
Patch76 marked this conversation as resolved.
Outdated

try:
yield calendar_entity, event_uid
finally:
# Best-effort cleanup; test body may have already deleted the event.
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}"
)
Comment on lines +284 to +293

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Avoid using broad except Exception in resource cleanup handlers. Following repository rules, use narrow exception handling to differentiate between failure types: log transport-level failures (like OSError) at debug level, but log command-level failures at warning level to surface potential resource leaks. Other exceptions should propagate to avoid masking programming bugs.

References
  1. In resource cleanup operations, use narrow exception handling and differentiate logging levels: log transport-level failures at debug level and command-level failures at warning level.
  2. Avoid using broad except Exception: pass in resource cleanup or teardown logic to prevent masking bugs.


async def test_create_calendar_event(self, mcp_client, cleanup_tracker):
"""
Test: Create a calendar event
Expand Down Expand Up @@ -307,43 +377,65 @@ async def test_create_calendar_event_invalid_entity(self, mcp_client):
)

assert data.get("success") is False, "Should fail for invalid entity"
assert "calendar." in str(
data.get("error", "")
), "Error should mention correct 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 entity create test completed")

async def test_delete_calendar_event(self, mcp_client):
async def test_delete_calendar_event(self, mcp_client, deletable_event_uid):
"""
Test: Delete a calendar event
Test: Delete a calendar event (positive + negative paths)

Tests the delete event functionality (may fail if no deletable events exist).
"""
calendar_entity = await self._find_writable_calendar(mcp_client)
if not calendar_entity:
pytest.skip("No calendar entities available for testing")
Creates a fresh event, deletes it (positive: hard-assert success), then
re-attempts deletion of the just-released UID (negative: hard-assert
failure with suggestions). UID-collision risk is eliminated because the
UID was just held and released by this test.

Assumes ha_config_remove_calendar_event raises on missing UID — current
behaviour for the _remove_* tool family. If the project later commits to
idempotent-success on missing (see #1412), the negative-path assertion
will need to flip.
"""
calendar_entity, event_uid = deletable_event_uid
logger.info(
f"Testing ha_config_remove_calendar_event for {calendar_entity}..."
f"Testing ha_config_remove_calendar_event for {calendar_entity} "
f"with uid={event_uid}..."
)

# Try to delete with a fake UID (will likely fail, but tests the API)
# Use safe_call_tool since we expect this to fail
data = await safe_call_tool(
# Positive path. Poll the delete to absorb the registration window
# after event creation on Local Calendar — the REST endpoint behind
# ha_config_get_calendar_events returns the UID before the
# calendar.delete_event service accepts it, empirically observed as
# a 400 on immediate delete-after-create. wait_for_tool_result
# retries on success=False until the predicate holds or the
# timeout fires.
first_delete = await wait_for_tool_result(
mcp_client,
"ha_config_remove_calendar_event",
{"entity_id": calendar_entity, "uid": "nonexistent-event-uid-xyz"},
{"entity_id": calendar_entity, "uid": event_uid},
predicate=lambda d: d.get("success") is True,
timeout=15,
description=f"first deletion of just-created event {event_uid}",
)
logger.info(f"Deleted event {event_uid} (positive path)")

# This will likely fail since the event doesn't exist
# We're mainly testing that the tool handles errors gracefully
if data.get("success"):
logger.info("Unexpectedly succeeded (event may have existed)")
else:
logger.info(f"Delete failed as expected: {data.get('error', 'Unknown')}")
assert data.get("error", {}).get("suggestions"), "Should provide helpful suggestions"

# Negative path: re-delete the released UID
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"
)
Comment thread
Patch76 marked this conversation as resolved.
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):
Expand All @@ -362,9 +454,9 @@ async def test_delete_calendar_event_invalid_entity(self, mcp_client):
)

assert data.get("success") is False, "Should fail for invalid entity"
assert "calendar." in str(
data.get("error", "")
), "Error should mention correct 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 entity delete test completed")
Expand All @@ -384,9 +476,9 @@ async def test_calendar_tools_overview(mcp_client):
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"
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
Expand All @@ -401,9 +493,9 @@ async def test_calendar_tools_overview(mcp_client):
"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"
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
Expand All @@ -412,9 +504,9 @@ async def test_calendar_tools_overview(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"
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")
Loading