425 lines
12 KiB
Python

"""Tests for calendar platform of Remote Calendar."""
from datetime import datetime
import pathlib
import textwrap
from httpx import Response
import pytest
import respx
from syrupy.assertion import SnapshotAssertion
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from . import setup_integration
from .conftest import (
CALENDER_URL,
FRIENDLY_NAME,
TEST_ENTITY,
GetEventsFn,
event_fields,
)
from tests.common import MockConfigEntry
# Test data files with known calendars from various sources. You can add a new file
# in the testdata directory and add it will be parsed and tested.
TESTDATA_FILES = sorted(
pathlib.Path("tests/components/remote_calendar/testdata/").glob("*.ics")
)
TESTDATA_IDS = [f.stem for f in TESTDATA_FILES]
@respx.mock
async def test_empty_calendar(
hass: HomeAssistant,
config_entry: MockConfigEntry,
get_events: GetEventsFn,
) -> None:
"""Test querying the API and fetching events."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=textwrap.dedent(
"""BEGIN:VCALENDAR
VERSION:2.0
PRODID:-//hacksw/handcal//NONSGML v1.0//EN
END:VCALENDAR
"""
),
)
)
await setup_integration(hass, config_entry)
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,
}
@pytest.mark.parametrize(
"ics_content",
[
textwrap.dedent(
"""\
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART;TZID=Europe/Berlin:19970714T190000
DTEND;TZID=Europe/Berlin:19970715T060000
END:VEVENT
END:VCALENDAR
"""
),
textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART:19970714T170000Z
DTEND:19970715T040000Z
END:VEVENT
END:VCALENDAR
"""
),
textwrap.dedent(
"""\
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART;TZID=America/Regina:19970714T110000
DTEND;TZID=America/Regina:19970714T220000
END:VEVENT
END:VCALENDAR
"""
),
textwrap.dedent(
"""\
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Bastille Day Party
DTSTART;TZID=America/Los_Angeles:19970714T100000
DTEND;TZID=America/Los_Angeles:19970714T210000
END:VEVENT
END:VCALENDAR
"""
),
],
)
@respx.mock
async def test_api_date_time_event(
get_events: GetEventsFn,
hass: HomeAssistant,
config_entry: MockConfigEntry,
ics_content: str,
) -> None:
"""Test an event with a start/end date time."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=ics_content,
)
)
await setup_integration(hass, config_entry)
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
@respx.mock
async def test_api_date_event(
get_events: GetEventsFn,
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test an event with a start/end date all day event."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=textwrap.dedent(
"""\
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Festival International de Jazz de Montreal
DTSTART:20070628
DTEND:20070709
END:VEVENT
END:VCALENDAR
"""
),
)
)
await setup_integration(hass, config_entry)
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
@pytest.mark.freeze_time(datetime(2007, 6, 28, 12))
@respx.mock
async def test_active_event(
get_events: GetEventsFn,
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test an event with a start/end date time."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=textwrap.dedent(
"""\
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Festival International de Jazz de Montreal
LOCATION:Montreal
DTSTART:20070628
DTEND:20070709
END:VEVENT
END:VCALENDAR
"""
),
)
)
await setup_integration(hass, config_entry)
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": "Festival International de Jazz de Montreal",
"all_day": True,
"description": "",
"location": "Montreal",
"start_time": "2007-06-28 00:00:00",
"end_time": "2007-07-09 00:00:00",
}
@pytest.mark.freeze_time(datetime(2007, 6, 27, 12))
@respx.mock
async def test_upcoming_event(
get_events: GetEventsFn,
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test an event with a start/end date time."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=textwrap.dedent(
"""\
BEGIN:VCALENDAR
VERSION:2.0
BEGIN:VEVENT
SUMMARY:Festival International de Jazz de Montreal
LOCATION:Montreal
DTSTART:20070628
DTEND:20070709
END:VEVENT
END:VCALENDAR
"""
),
)
)
await setup_integration(hass, config_entry)
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": "Festival International de Jazz de Montreal",
"all_day": True,
"description": "",
"location": "Montreal",
"start_time": "2007-06-28 00:00:00",
"end_time": "2007-07-09 00:00:00",
}
@respx.mock
async def test_recurring_event(
get_events: GetEventsFn,
hass: HomeAssistant,
config_entry: MockConfigEntry,
) -> None:
"""Test an event with a recurrence rule."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20220829T090000
DTEND:20220829T100000
SUMMARY:Monday meeting
RRULE:FREQ=WEEKLY;BYDAY=MO
END:VEVENT
END:VCALENDAR
"""
),
)
)
await setup_integration(hass, config_entry)
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",
},
]
@respx.mock
@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(
get_events: GetEventsFn,
hass: HomeAssistant,
config_entry: MockConfigEntry,
event_order: list[str],
) -> None:
"""Test the sort order of an all day events depending on the time zone."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=textwrap.dedent(
"""\
BEGIN:VCALENDAR
BEGIN:VEVENT
DTSTART:20221008
DTEND:20221009
SUMMARY:All Day Event
END:VEVENT
BEGIN:VEVENT
DTSTART:20221007T230000Z
DTEND:20221008T233000Z
SUMMARY:One
END:VEVENT
BEGIN:VEVENT
DTSTART:20221008T010000Z
DTEND:20221008T020000Z
SUMMARY:Two
END:VEVENT
END:VCALENDAR
"""
),
)
)
await setup_integration(hass, config_entry)
events = await get_events("2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z")
assert [event["summary"] for event in events] == event_order
@respx.mock
@pytest.mark.parametrize("ics_filename", TESTDATA_FILES, ids=TESTDATA_IDS)
async def test_calendar_examples(
hass: HomeAssistant,
config_entry: MockConfigEntry,
get_events: GetEventsFn,
ics_filename: pathlib.Path,
snapshot: SnapshotAssertion,
) -> None:
"""Test parsing known calendars form test data files."""
respx.get(CALENDER_URL).mock(
return_value=Response(
status_code=200,
text=ics_filename.read_text(),
)
)
await setup_integration(hass, config_entry)
events = await get_events("1997-07-14T00:00:00", "2025-07-01T00:00:00")
assert events == snapshot