Files

1219 lines
37 KiB
Python

"""Tests for calendar platform of local calendar."""
import datetime
from datetime import timedelta
import textwrap
from unittest.mock import patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from homeassistant.components.local_calendar.const import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.helpers.template import DATE_STR_FORMAT
from homeassistant.setup import async_setup_component
from homeassistant.util import dt as dt_util
from .conftest import (
FRIENDLY_NAME,
TEST_ENTITY,
ClientFixture,
GetEventsFn,
event_fields,
)
from tests.common import MockConfigEntry, async_fire_time_changed
async def test_empty_calendar(
hass: HomeAssistant, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test querying the API and fetching events."""
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert len(events) == 0
state = hass.states.get(TEST_ENTITY)
assert state
assert state.name == FRIENDLY_NAME
assert state.state == STATE_OFF
assert dict(state.attributes) == {
"friendly_name": FRIENDLY_NAME,
"supported_features": 7,
}
@pytest.mark.parametrize(
("dtstart", "dtend"),
[
("1997-07-14T18:00:00+01:00", "1997-07-15T05:00:00+01:00"),
("1997-07-14T17:00:00+00:00", "1997-07-15T04:00:00+00:00"),
("1997-07-14T11:00:00-06:00", "1997-07-14T22:00:00-06:00"),
("1997-07-14T10:00:00-07:00", "1997-07-14T21:00:00-07:00"),
],
)
async def test_api_date_time_event(
ws_client: ClientFixture,
setup_integration: None,
get_events: GetEventsFn,
dtstart: str,
dtend: str,
) -> None:
"""Test an event with a start/end date time.
Events created in various timezones are ultimately returned relative
to local home assistant timezone.
"""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": dtstart,
"dtend": dtend,
},
},
)
events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
}
]
# Query events in UTC
# Time range before event
events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T16:00:00Z")
assert len(events) == 0
# Time range after event
events = await get_events("1997-07-15T05:00:00Z", "1997-07-15T06:00:00Z")
assert len(events) == 0
# Overlap with event start
events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T18:00:00Z")
assert len(events) == 1
# Overlap with event end
events = await get_events("1997-07-15T03:00:00Z", "1997-07-15T06:00:00Z")
assert len(events) == 1
# Query events overlapping with start and end but in another timezone
events = await get_events("1997-07-12T23:00:00-01:00", "1997-07-14T17:00:00-01:00")
assert len(events) == 1
events = await get_events("1997-07-15T02:00:00-01:00", "1997-07-15T05:00:00-01:00")
assert len(events) == 1
async def test_api_date_event(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test an event with a start/end date all day event."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Festival International de Jazz de Montreal",
"dtstart": "2007-06-28",
"dtend": "2007-07-09",
},
},
)
events = await get_events("2007-06-20T00:00:00", "2007-07-20T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Festival International de Jazz de Montreal",
"start": {"date": "2007-06-28"},
"end": {"date": "2007-07-09"},
}
]
# Time range before event (timezone is -6)
events = await get_events("2007-06-26T00:00:00Z", "2007-06-28T01:00:00Z")
assert len(events) == 0
# Time range after event
events = await get_events("2007-07-10T00:00:00Z", "2007-07-11T00:00:00Z")
assert len(events) == 0
# Overlap with event start (timezone is -6)
events = await get_events("2007-06-26T00:00:00Z", "2007-06-28T08:00:00Z")
assert len(events) == 1
# Overlap with event end
events = await get_events("2007-07-09T00:00:00Z", "2007-07-11T00:00:00Z")
assert len(events) == 1
async def test_active_event(
hass: HomeAssistant,
ws_client: ClientFixture,
setup_integration: None,
) -> None:
"""Test an event with a start/end date time."""
start = dt_util.now() - datetime.timedelta(minutes=30)
end = dt_util.now() + datetime.timedelta(minutes=30)
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Evening lights",
"dtstart": start.isoformat(),
"dtend": end.isoformat(),
},
},
)
state = hass.states.get(TEST_ENTITY)
assert state
assert state.name == FRIENDLY_NAME
assert state.state == STATE_ON
assert dict(state.attributes) == {
"friendly_name": FRIENDLY_NAME,
"message": "Evening lights",
"all_day": False,
"description": "",
"location": "",
"start_time": start.strftime(DATE_STR_FORMAT),
"end_time": end.strftime(DATE_STR_FORMAT),
"supported_features": 7,
}
async def test_upcoming_event(
hass: HomeAssistant,
ws_client: ClientFixture,
setup_integration: None,
) -> None:
"""Test an event with a start/end date time."""
start = dt_util.now() + datetime.timedelta(days=1)
end = dt_util.now() + datetime.timedelta(days=1, hours=1)
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Evening lights",
"dtstart": start.isoformat(),
"dtend": end.isoformat(),
},
},
)
state = hass.states.get(TEST_ENTITY)
assert state
assert state.name == FRIENDLY_NAME
assert state.state == STATE_OFF
assert dict(state.attributes) == {
"friendly_name": FRIENDLY_NAME,
"message": "Evening lights",
"all_day": False,
"description": "",
"location": "",
"start_time": start.strftime(DATE_STR_FORMAT),
"end_time": end.strftime(DATE_STR_FORMAT),
"supported_features": 7,
}
async def test_recurring_event(
ws_client: ClientFixture,
setup_integration: None,
hass: HomeAssistant,
get_events: GetEventsFn,
) -> None:
"""Test an event with a recurrence rule."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Monday meeting",
"dtstart": "2022-08-29T09:00:00",
"dtend": "2022-08-29T10:00:00",
"rrule": "FREQ=WEEKLY",
},
},
)
events = await get_events("2022-08-20T00:00:00", "2022-09-20T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Monday meeting",
"start": {"dateTime": "2022-08-29T09:00:00-06:00"},
"end": {"dateTime": "2022-08-29T10:00:00-06:00"},
"recurrence_id": "20220829T090000",
},
{
"summary": "Monday meeting",
"start": {"dateTime": "2022-09-05T09:00:00-06:00"},
"end": {"dateTime": "2022-09-05T10:00:00-06:00"},
"recurrence_id": "20220905T090000",
},
{
"summary": "Monday meeting",
"start": {"dateTime": "2022-09-12T09:00:00-06:00"},
"end": {"dateTime": "2022-09-12T10:00:00-06:00"},
"recurrence_id": "20220912T090000",
},
{
"summary": "Monday meeting",
"start": {"dateTime": "2022-09-19T09:00:00-06:00"},
"end": {"dateTime": "2022-09-19T10:00:00-06:00"},
"recurrence_id": "20220919T090000",
},
]
@pytest.mark.parametrize(
"time_zone",
["America/Los_Angeles", "America/New_York", "Europe/London", "Asia/Tokyo"],
)
async def test_recurring_event_with_until_non_utc_timezone(
ws_client: ClientFixture,
setup_integration: None,
hass: HomeAssistant,
get_events: GetEventsFn,
) -> None:
"""Test creating a recurring event with UNTIL in a non-UTC timezone.
Regression test: When HA is configured with a non-UTC timezone, creating
a recurring event with an UNTIL date was failing because _parse_event()
strips tzinfo from dtstart (making it floating) but was not stripping
tzinfo from UNTIL in the rrule, causing a naive/aware datetime mismatch.
"""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Weekly meeting",
"dtstart": "2022-08-29T09:00:00+00:00",
"dtend": "2022-08-29T10:00:00+00:00",
"rrule": "FREQ=WEEKLY;UNTIL=20220912T235959Z",
},
},
)
events = await get_events("2022-08-20T00:00:00", "2022-09-20T00:00:00")
summaries = [event["summary"] for event in events]
assert summaries.count("Weekly meeting") == 3
async def test_websocket_delete(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test websocket delete command."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": "1997-07-14T17:00:00+00:00",
"dtend": "1997-07-15T04:00:00+00:00",
},
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
}
]
uid = events[0]["uid"]
# Delete the event
await client.cmd_result(
"delete",
{
"entity_id": TEST_ENTITY,
"uid": uid,
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == []
async def test_websocket_delete_recurring(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test deleting a recurring event."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Morning Routine",
"dtstart": "2022-08-22T08:30:00",
"dtend": "2022-08-22T09:00:00",
"rrule": "FREQ=DAILY",
},
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-23T08:30:00-06:00"},
"end": {"dateTime": "2022-08-23T09:00:00-06:00"},
"recurrence_id": "20220823T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-24T08:30:00-06:00"},
"end": {"dateTime": "2022-08-24T09:00:00-06:00"},
"recurrence_id": "20220824T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-25T08:30:00-06:00"},
"end": {"dateTime": "2022-08-25T09:00:00-06:00"},
"recurrence_id": "20220825T083000",
},
]
uid = events[0]["uid"]
assert [event["uid"] for event in events] == [uid] * 4
# Cancel a single instance and confirm it was removed
await client.cmd_result(
"delete",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"recurrence_id": "20220824T083000",
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-23T08:30:00-06:00"},
"end": {"dateTime": "2022-08-23T09:00:00-06:00"},
"recurrence_id": "20220823T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-25T08:30:00-06:00"},
"end": {"dateTime": "2022-08-25T09:00:00-06:00"},
"recurrence_id": "20220825T083000",
},
]
# Delete all and future and confirm multiple were removed
await client.cmd_result(
"delete",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"recurrence_id": "20220823T083000",
"recurrence_range": "THISANDFUTURE",
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
]
async def test_websocket_delete_empty_recurrence_id(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test websocket delete command with an empty recurrence id no-op."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": "1997-07-14T17:00:00+00:00",
"dtend": "1997-07-15T04:00:00+00:00",
},
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
}
]
uid = events[0]["uid"]
# Delete the event with an empty recurrence id
await client.cmd_result(
"delete",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"recurrence_id": "",
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == []
async def test_websocket_update(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test websocket update command."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": "1997-07-14T17:00:00+00:00",
"dtend": "1997-07-15T04:00:00+00:00",
},
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
}
]
uid = events[0]["uid"]
# Update the event
await client.cmd_result(
"update",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"event": {
"summary": "Bastille Day Party [To be rescheduled]",
"dtstart": "1997-07-14",
"dtend": "1997-07-15",
},
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party [To be rescheduled]",
"start": {"date": "1997-07-14"},
"end": {"date": "1997-07-15"},
}
]
async def test_websocket_update_empty_recurrence(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test an edit with an empty recurrence id (no-op)."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": "1997-07-14T17:00:00+00:00",
"dtend": "1997-07-15T04:00:00+00:00",
},
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
}
]
uid = events[0]["uid"]
# Update the event with an empty string for the recurrence id which should
# have no effect.
await client.cmd_result(
"update",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"recurrence_id": "",
"event": {
"summary": "Bastille Day Party [To be rescheduled]",
"dtstart": "1997-07-15T11:00:00-06:00",
"dtend": "1997-07-15T22:00:00-06:00",
},
},
)
events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party [To be rescheduled]",
"start": {"dateTime": "1997-07-15T11:00:00-06:00"},
"end": {"dateTime": "1997-07-15T22:00:00-06:00"},
}
]
async def test_websocket_update_recurring_this_and_future(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test updating a recurring event."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Morning Routine",
"dtstart": "2022-08-22T08:30:00",
"dtend": "2022-08-22T09:00:00",
"rrule": "FREQ=DAILY",
},
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-23T08:30:00-06:00"},
"end": {"dateTime": "2022-08-23T09:00:00-06:00"},
"recurrence_id": "20220823T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-24T08:30:00-06:00"},
"end": {"dateTime": "2022-08-24T09:00:00-06:00"},
"recurrence_id": "20220824T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-25T08:30:00-06:00"},
"end": {"dateTime": "2022-08-25T09:00:00-06:00"},
"recurrence_id": "20220825T083000",
},
]
uid = events[0]["uid"]
assert [event["uid"] for event in events] == [uid] * 4
# Update a single instance and confirm the change is reflected
await client.cmd_result(
"update",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"recurrence_id": "20220824T083000",
"recurrence_range": "THISANDFUTURE",
"event": {
"summary": "Morning Routine [Adjusted]",
"dtstart": "2022-08-24T08:00:00",
"dtend": "2022-08-24T08:30:00",
},
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-23T08:30:00-06:00"},
"end": {"dateTime": "2022-08-23T09:00:00-06:00"},
"recurrence_id": "20220823T083000",
},
{
"summary": "Morning Routine [Adjusted]",
"start": {"dateTime": "2022-08-24T08:00:00-06:00"},
"end": {"dateTime": "2022-08-24T08:30:00-06:00"},
"recurrence_id": "20220824T080000",
},
{
"summary": "Morning Routine [Adjusted]",
"start": {"dateTime": "2022-08-25T08:00:00-06:00"},
"end": {"dateTime": "2022-08-25T08:30:00-06:00"},
"recurrence_id": "20220825T080000",
},
]
async def test_websocket_update_recurring(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Test updating a recurring event."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Morning Routine",
"dtstart": "2022-08-22T08:30:00",
"dtend": "2022-08-22T09:00:00",
"rrule": "FREQ=DAILY",
},
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-23T08:30:00-06:00"},
"end": {"dateTime": "2022-08-23T09:00:00-06:00"},
"recurrence_id": "20220823T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-24T08:30:00-06:00"},
"end": {"dateTime": "2022-08-24T09:00:00-06:00"},
"recurrence_id": "20220824T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-25T08:30:00-06:00"},
"end": {"dateTime": "2022-08-25T09:00:00-06:00"},
"recurrence_id": "20220825T083000",
},
]
uid = events[0]["uid"]
assert [event["uid"] for event in events] == [uid] * 4
# Update a single instance and confirm the change is reflected
await client.cmd_result(
"update",
{
"entity_id": TEST_ENTITY,
"uid": uid,
"recurrence_id": "20220824T083000",
"event": {
"summary": "Morning Routine [Adjusted]",
"dtstart": "2022-08-24T08:00:00",
"dtend": "2022-08-24T08:30:00",
},
},
)
events = await get_events("2022-08-22T00:00:00", "2022-08-26T00:00:00")
assert list(map(event_fields, events)) == [
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-22T08:30:00-06:00"},
"end": {"dateTime": "2022-08-22T09:00:00-06:00"},
"recurrence_id": "20220822T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-23T08:30:00-06:00"},
"end": {"dateTime": "2022-08-23T09:00:00-06:00"},
"recurrence_id": "20220823T083000",
},
{
"summary": "Morning Routine [Adjusted]",
"start": {"dateTime": "2022-08-24T08:00:00-06:00"},
"end": {"dateTime": "2022-08-24T08:30:00-06:00"},
"recurrence_id": "20220824T083000",
},
{
"summary": "Morning Routine",
"start": {"dateTime": "2022-08-25T08:30:00-06:00"},
"end": {"dateTime": "2022-08-25T09:00:00-06:00"},
"recurrence_id": "20220825T083000",
},
]
@pytest.mark.parametrize(
"rrule",
[
"FREQ=SECONDLY",
"FREQ=MINUTELY",
"FREQ=HOURLY",
"invalid",
"",
],
)
async def test_invalid_rrule(
ws_client: ClientFixture,
setup_integration: None,
hass: HomeAssistant,
get_events: GetEventsFn,
rrule: str,
) -> None:
"""Test an event with a recurrence rule."""
client = await ws_client()
resp = await client.cmd(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Monday meeting",
"dtstart": "2022-08-29T09:00:00",
"dtend": "2022-08-29T10:00:00",
"rrule": rrule,
},
},
)
assert resp
assert not resp.get("success")
assert "error" in resp
assert resp["error"].get("code") == "invalid_format"
@pytest.mark.parametrize(
("time_zone", "event_order"),
[
("America/Los_Angeles", ["One", "Two", "All Day Event"]),
("America/Regina", ["One", "Two", "All Day Event"]),
("UTC", ["One", "All Day Event", "Two"]),
("Asia/Tokyo", ["All Day Event", "One", "Two"]),
],
)
async def test_all_day_iter_order(
hass: HomeAssistant,
ws_client: ClientFixture,
setup_integration: None,
get_events: GetEventsFn,
event_order: list[str],
) -> None:
"""Test the sort order of an all day events depending on the time zone."""
client = await ws_client()
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "All Day Event",
"dtstart": "2022-10-08",
"dtend": "2022-10-09",
},
},
)
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "One",
"dtstart": "2022-10-07T23:00:00+00:00",
"dtend": "2022-10-07T23:30:00+00:00",
},
},
)
await client.cmd_result(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Two",
"dtstart": "2022-10-08T01:00:00+00:00",
"dtend": "2022-10-08T02:00:00+00:00",
},
},
)
events = await get_events("2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z")
assert [event["summary"] for event in events] == event_order
async def test_start_end_types(
ws_client: ClientFixture,
setup_integration: None,
) -> None:
"""Test a start and end with different date and date time types."""
client = await ws_client()
result = await client.cmd(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": "1997-07-15",
"dtend": "1997-07-14T17:00:00+00:00",
},
},
)
assert result
assert not result.get("success")
assert "error" in result
assert "code" in result["error"]
assert result["error"]["code"] == "invalid_format"
async def test_end_before_start(
ws_client: ClientFixture,
setup_integration: None,
) -> None:
"""Test an event with a start/end date time."""
client = await ws_client()
result = await client.cmd(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
"dtstart": "1997-07-15T04:00:00+00:00",
"dtend": "1997-07-14T17:00:00+00:00",
},
},
)
assert result
assert not result.get("success")
assert "error" in result
assert "code" in result["error"]
assert result["error"]["code"] == "invalid_format"
async def test_invalid_recurrence_rule(
ws_client: ClientFixture,
setup_integration: None,
) -> None:
"""Test an event with a recurrence rule."""
client = await ws_client()
result = await client.cmd(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Monday meeting",
"dtstart": "2022-08-29T09:00:00",
"dtend": "2022-08-29T10:00:00",
"rrule": "FREQ=invalid;'",
},
},
)
assert result
assert not result.get("success")
assert "error" in result
assert "code" in result["error"]
assert result["error"]["code"] == "invalid_format"
async def test_invalid_date_formats(
ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn
) -> None:
"""Exercises a validation error within rfc5545 parsing in ical."""
client = await ws_client()
result = await client.cmd(
"create",
{
"entity_id": TEST_ENTITY,
"event": {
"summary": "Bastille Day Party",
# Can't mix offset aware and floating dates
"dtstart": "1997-07-15T04:00:00+08:00",
"dtend": "1997-07-14T17:00:00",
},
},
)
assert result
assert not result.get("success")
assert "error" in result
assert "code" in result["error"]
assert result["error"]["code"] == "invalid_format"
async def test_update_invalid_event_id(
ws_client: ClientFixture,
setup_integration: None,
hass: HomeAssistant,
) -> None:
"""Test updating an event with an invalid event uid."""
client = await ws_client()
resp = await client.cmd(
"update",
{
"entity_id": TEST_ENTITY,
"uid": "uid-does-not-exist",
"event": {
"summary": "Bastille Day Party [To be rescheduled]",
"dtstart": "1997-07-14",
"dtend": "1997-07-15",
},
},
)
assert resp
assert not resp.get("success")
assert "error" in resp
assert resp["error"].get("code") == "failed"
async def test_delete_invalid_event_id(
ws_client: ClientFixture,
setup_integration: None,
hass: HomeAssistant,
) -> None:
"""Test deleting an event with an invalid event uid."""
client = await ws_client()
resp = await client.cmd(
"delete",
{
"entity_id": TEST_ENTITY,
"uid": "uid-does-not-exist",
},
)
assert resp
assert not resp.get("success")
assert "error" in resp
assert resp["error"].get("code") == "failed"
@pytest.mark.parametrize(
("start_date_time", "end_date_time"),
[
("1997-07-14T17:00:00+00:00", "1997-07-15T04:00:00+00:00"),
("1997-07-14T11:00:00-06:00", "1997-07-14T22:00:00-06:00"),
],
)
async def test_create_event_service(
hass: HomeAssistant,
setup_integration: None,
get_events: GetEventsFn,
start_date_time: str,
end_date_time: str,
config_entry: MockConfigEntry,
) -> None:
"""Test creating an event using the create_event service."""
await hass.services.async_call(
"calendar",
"create_event",
{
"start_date_time": start_date_time,
"end_date_time": end_date_time,
"summary": "Bastille Day Party",
"location": "Test Location",
},
target={"entity_id": TEST_ENTITY},
blocking=True,
)
# Ensure data is written to disk
await hass.async_block_till_done()
events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
"location": "Test Location",
}
]
events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T18:00:00Z")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
"location": "Test Location",
}
]
# Reload the config entry, which reloads the content from the store and
# verifies that the persisted data can be parsed correctly.
await hass.config_entries.async_reload(config_entry.entry_id)
await hass.async_block_till_done()
events = await get_events("1997-07-13T00:00:00Z", "1997-07-14T18:00:00Z")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T22:00:00-06:00"},
"location": "Test Location",
}
]
@pytest.mark.parametrize(
"ics_content",
[
textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART:19970714
DTEND:19970714
END:VEVENT
END:VCALENDAR
"""
),
textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART:19970714
DTEND:19970710
END:VEVENT
END:VCALENDAR
"""
),
],
ids=["no_duration", "negative"],
)
async def test_invalid_all_day_event(
ws_client: ClientFixture,
setup_integration: None,
get_events: GetEventsFn,
) -> None:
"""Test all day events with invalid durations, which are coerced to be valid."""
events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"date": "1997-07-14"},
"end": {"date": "1997-07-15"},
}
]
@pytest.mark.parametrize(
"ics_content",
[
textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART:19970714T110000
DTEND:19970714T110000
END:VEVENT
END:VCALENDAR
"""
),
textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART:19970714T110000
DTEND:19970710T100000
END:VEVENT
END:VCALENDAR
"""
),
],
ids=["no_duration", "negative"],
)
async def test_invalid_event_duration(
ws_client: ClientFixture,
setup_integration: None,
get_events: GetEventsFn,
) -> None:
"""Test events with invalid durations, which are coerced to be valid."""
events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z")
assert list(map(event_fields, events)) == [
{
"summary": "Bastille Day Party",
"start": {"dateTime": "1997-07-14T11:00:00-06:00"},
"end": {"dateTime": "1997-07-14T11:30:00-06:00"},
}
]
ADJACENT_EVENTS_ICS = """BEGIN:VCALENDAR
PRODID:-//homeassistant.io//local_calendar 1.0//EN
VERSION:2.0
BEGIN:VEVENT
DTSTART:20260729T014500
DTEND:20260729T020000
SUMMARY:First
UID:first
END:VEVENT
BEGIN:VEVENT
DTSTART:20260729T020000
DTEND:20260729T021500
SUMMARY:Second
UID:second
END:VEVENT
END:VCALENDAR
"""
@pytest.mark.parametrize("ics_content", [ADJACENT_EVENTS_ICS])
async def test_adjacent_events_stay_on(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
config_entry: MockConfigEntry,
) -> None:
"""Test the state stays on when one event ends as the next one begins.
The scan interval is widened so the platform poll cannot reach the boundary
first: what is under test is the alarm scheduled for the end of the current
event, which has to be able to pick up the next one on its own.
"""
freezer.move_to("2026-07-29 07:50:20+00:00") # 01:50:20 in America/Regina
config_entry.add_to_hass(hass)
with patch("homeassistant.components.calendar.SCAN_INTERVAL", timedelta(hours=1)):
assert await async_setup_component(hass, DOMAIN, {})
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY)
assert state.state == STATE_ON
assert state.attributes["message"] == "First"
# 02:00:00 in America/Regina, the moment the first event ends and the
# second begins.
freezer.move_to("2026-07-29 08:00:00+00:00")
async_fire_time_changed(hass, dt_util.utcnow())
await hass.async_block_till_done()
state = hass.states.get(TEST_ENTITY)
assert state.state == STATE_ON
assert state.attributes["message"] == "Second"