zha and plex still add the same config entry multiple times but they are going to need seperate PRs as they have more complex logic
1348 lines
42 KiB
Python
1348 lines
42 KiB
Python
"""The tests for the google calendar platform."""
|
|
from __future__ import annotations
|
|
|
|
from collections.abc import Awaitable, Callable
|
|
import datetime
|
|
from http import HTTPStatus
|
|
from typing import Any
|
|
from unittest.mock import patch
|
|
import urllib
|
|
|
|
from aiohttp.client_exceptions import ClientError
|
|
from freezegun.api import FrozenDateTimeFactory
|
|
from gcal_sync.auth import API_BASE_URL
|
|
import pytest
|
|
|
|
from homeassistant.components.google.const import CONF_CALENDAR_ACCESS, DOMAIN
|
|
from homeassistant.const import STATE_OFF, STATE_ON, Platform
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.helpers import entity_registry as er
|
|
from homeassistant.helpers.template import DATE_STR_FORMAT
|
|
import homeassistant.util.dt as dt_util
|
|
|
|
from .conftest import (
|
|
CALENDAR_ID,
|
|
TEST_API_ENTITY,
|
|
TEST_API_ENTITY_NAME,
|
|
TEST_EVENT,
|
|
TEST_YAML_ENTITY,
|
|
TEST_YAML_ENTITY_NAME,
|
|
ApiResult,
|
|
ComponentSetup,
|
|
)
|
|
|
|
from tests.common import async_fire_time_changed
|
|
from tests.test_util.aiohttp import AiohttpClientMocker
|
|
from tests.typing import ClientSessionGenerator, WebSocketGenerator
|
|
|
|
TEST_ENTITY = TEST_API_ENTITY
|
|
TEST_ENTITY_NAME = TEST_API_ENTITY_NAME
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_test_setup(
|
|
test_api_calendar,
|
|
mock_calendars_list,
|
|
):
|
|
"""Fixture that sets up the default API responses during integration setup."""
|
|
mock_calendars_list({"items": [test_api_calendar]})
|
|
|
|
|
|
def get_events_url(entity: str, start: str, end: str) -> str:
|
|
"""Create a url to get events during the specified time range."""
|
|
return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}"
|
|
|
|
|
|
def upcoming() -> dict[str, Any]:
|
|
"""Create a test event with an arbitrary start/end time fetched from the api url."""
|
|
now = dt_util.now()
|
|
return {
|
|
"start": {"dateTime": now.isoformat()},
|
|
"end": {"dateTime": (now + datetime.timedelta(minutes=5)).isoformat()},
|
|
}
|
|
|
|
|
|
def upcoming_event_url(entity: str = TEST_ENTITY) -> str:
|
|
"""Return a calendar API to return events created by upcoming()."""
|
|
now = dt_util.now()
|
|
start = (now - datetime.timedelta(minutes=60)).isoformat()
|
|
end = (now + datetime.timedelta(minutes=60)).isoformat()
|
|
return get_events_url(entity, start, end)
|
|
|
|
|
|
class Client:
|
|
"""Test client with helper methods for calendar websocket."""
|
|
|
|
def __init__(self, client):
|
|
"""Initialize Client."""
|
|
self.client = client
|
|
self.id = 0
|
|
|
|
async def cmd(self, cmd: str, payload: dict[str, Any] = None) -> dict[str, Any]:
|
|
"""Send a command and receive the json result."""
|
|
self.id += 1
|
|
await self.client.send_json(
|
|
{
|
|
"id": self.id,
|
|
"type": f"calendar/event/{cmd}",
|
|
**(payload if payload is not None else {}),
|
|
}
|
|
)
|
|
resp = await self.client.receive_json()
|
|
assert resp.get("id") == self.id
|
|
return resp
|
|
|
|
async def cmd_result(self, cmd: str, payload: dict[str, Any] = None) -> Any:
|
|
"""Send a command and parse the result."""
|
|
resp = await self.cmd(cmd, payload)
|
|
assert resp.get("success")
|
|
assert resp.get("type") == "result"
|
|
return resp.get("result")
|
|
|
|
|
|
ClientFixture = Callable[[], Awaitable[Client]]
|
|
|
|
|
|
@pytest.fixture
|
|
async def ws_client(
|
|
hass: HomeAssistant,
|
|
hass_ws_client: WebSocketGenerator,
|
|
) -> ClientFixture:
|
|
"""Fixture for creating the test websocket client."""
|
|
|
|
async def create_client() -> Client:
|
|
ws_client = await hass_ws_client(hass)
|
|
return Client(ws_client)
|
|
|
|
return create_client
|
|
|
|
|
|
async def test_all_day_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test for an all day calendar event."""
|
|
week_from_today = dt_util.now().date() + datetime.timedelta(days=7)
|
|
end_event = week_from_today + datetime.timedelta(days=1)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": week_from_today.isoformat()},
|
|
"end": {"date": end_event.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": True,
|
|
"offset_reached": False,
|
|
"start_time": week_from_today.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_future_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test for an upcoming event."""
|
|
one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
|
end_event = one_hour_from_now + datetime.timedelta(minutes=60)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": one_hour_from_now.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": one_hour_from_now.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_in_progress_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test an event that is active now."""
|
|
middle_of_event = dt_util.now() - datetime.timedelta(minutes=30)
|
|
end_event = middle_of_event + datetime.timedelta(minutes=60)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": middle_of_event.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_ON
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": middle_of_event.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_offset_in_progress_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test an event that is active now with an offset."""
|
|
middle_of_event = dt_util.now() + datetime.timedelta(minutes=14)
|
|
end_event = middle_of_event + datetime.timedelta(minutes=60)
|
|
event_summary = "Test Event in Progress"
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": middle_of_event.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
"summary": f"{event_summary} !!-15",
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event_summary,
|
|
"all_day": False,
|
|
"offset_reached": True,
|
|
"start_time": middle_of_event.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_all_day_offset_in_progress_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test an all day event that is currently in progress due to an offset."""
|
|
tomorrow = dt_util.now().date() + datetime.timedelta(days=1)
|
|
end_event = tomorrow + datetime.timedelta(days=1)
|
|
event_summary = "Test All Day Event Offset In Progress"
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": tomorrow.isoformat()},
|
|
"end": {"date": end_event.isoformat()},
|
|
"summary": f"{event_summary} !!-25:0",
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event_summary,
|
|
"all_day": True,
|
|
"offset_reached": True,
|
|
"start_time": tomorrow.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_all_day_offset_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test an all day event that not in progress due to an offset."""
|
|
now = dt_util.now()
|
|
day_after_tomorrow = now.date() + datetime.timedelta(days=2)
|
|
end_event = day_after_tomorrow + datetime.timedelta(days=1)
|
|
offset_hours = 1 + now.hour
|
|
event_summary = "Test All Day Event Offset"
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": day_after_tomorrow.isoformat()},
|
|
"end": {"date": end_event.isoformat()},
|
|
"summary": f"{event_summary} !!-{offset_hours}:0",
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event_summary,
|
|
"all_day": True,
|
|
"offset_reached": False,
|
|
"start_time": day_after_tomorrow.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_missing_summary(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test that a summary is optional."""
|
|
start_event = dt_util.now() + datetime.timedelta(minutes=14)
|
|
end_event = start_event + datetime.timedelta(minutes=60)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": start_event.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
}
|
|
del event["summary"]
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": "",
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": start_event.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_update_error(
|
|
hass: HomeAssistant,
|
|
component_setup,
|
|
mock_events_list,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test that the calendar update handles a server error."""
|
|
|
|
now = dt_util.now()
|
|
mock_events_list(
|
|
{
|
|
"items": [
|
|
{
|
|
**TEST_EVENT,
|
|
"start": {
|
|
"dateTime": (now + datetime.timedelta(minutes=-30)).isoformat()
|
|
},
|
|
"end": {
|
|
"dateTime": (now + datetime.timedelta(minutes=30)).isoformat()
|
|
},
|
|
}
|
|
]
|
|
}
|
|
)
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == "on"
|
|
|
|
# Advance time to next data update interval
|
|
now += datetime.timedelta(minutes=30)
|
|
|
|
aioclient_mock.clear_requests()
|
|
mock_events_list({}, exc=ClientError())
|
|
|
|
with patch("homeassistant.util.utcnow", return_value=now):
|
|
async_fire_time_changed(hass, now)
|
|
await hass.async_block_till_done()
|
|
|
|
# Entity is marked uanvailable due to API failure
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == "unavailable"
|
|
|
|
# Advance time past next coordinator update
|
|
now += datetime.timedelta(minutes=30)
|
|
|
|
aioclient_mock.clear_requests()
|
|
mock_events_list(
|
|
{
|
|
"items": [
|
|
{
|
|
**TEST_EVENT,
|
|
"start": {
|
|
"dateTime": (now + datetime.timedelta(minutes=30)).isoformat()
|
|
},
|
|
"end": {
|
|
"dateTime": (now + datetime.timedelta(minutes=60)).isoformat()
|
|
},
|
|
}
|
|
]
|
|
}
|
|
)
|
|
|
|
with patch("homeassistant.util.utcnow", return_value=now):
|
|
async_fire_time_changed(hass, now)
|
|
await hass.async_block_till_done()
|
|
|
|
# State updated with new API response
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == "off"
|
|
|
|
|
|
async def test_calendars_api(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
component_setup,
|
|
mock_events_list_items,
|
|
) -> None:
|
|
"""Test the Rest API returns the calendar."""
|
|
mock_events_list_items([])
|
|
assert await component_setup()
|
|
|
|
client = await hass_client()
|
|
response = await client.get("/api/calendars")
|
|
assert response.status == HTTPStatus.OK
|
|
data = await response.json()
|
|
assert data == [
|
|
{
|
|
"entity_id": TEST_ENTITY,
|
|
"name": TEST_ENTITY_NAME,
|
|
}
|
|
]
|
|
|
|
|
|
async def test_http_event_api_failure(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
component_setup,
|
|
mock_calendars_list,
|
|
mock_events_list,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test the Rest API response during a calendar failure."""
|
|
mock_events_list({}, exc=ClientError())
|
|
|
|
assert await component_setup()
|
|
|
|
client = await hass_client()
|
|
|
|
response = await client.get(upcoming_event_url())
|
|
assert response.status == HTTPStatus.INTERNAL_SERVER_ERROR
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == "unavailable"
|
|
|
|
|
|
@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00")
|
|
async def test_http_api_event(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
) -> None:
|
|
"""Test querying the API and fetching events from the server."""
|
|
hass.config.set_time_zone("Asia/Baghdad")
|
|
event = {
|
|
**TEST_EVENT,
|
|
**upcoming(),
|
|
}
|
|
mock_events_list_items([event])
|
|
assert await component_setup()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(upcoming_event_url())
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert len(events) == 1
|
|
assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == {
|
|
"summary": TEST_EVENT["summary"],
|
|
"start": {"dateTime": "2022-03-27T15:05:00+03:00"},
|
|
"end": {"dateTime": "2022-03-27T15:10:00+03:00"},
|
|
}
|
|
|
|
|
|
@pytest.mark.freeze_time("2022-03-27 12:05:00+00:00")
|
|
async def test_http_api_all_day_event(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
) -> None:
|
|
"""Test querying the API and fetching events from the server."""
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": "2022-03-27"},
|
|
"end": {"date": "2022-03-28"},
|
|
}
|
|
mock_events_list_items([event])
|
|
assert await component_setup()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(upcoming_event_url())
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert len(events) == 1
|
|
assert {k: events[0].get(k) for k in ["summary", "start", "end"]} == {
|
|
"summary": TEST_EVENT["summary"],
|
|
"start": {"date": "2022-03-27"},
|
|
"end": {"date": "2022-03-28"},
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("calendars_config_ignore_availability", "transparency", "expect_visible_event"),
|
|
[
|
|
# Look at visibility to determine if entity is created
|
|
(False, "opaque", True),
|
|
(False, "transparent", False),
|
|
# Ignoring availability and always show the entity
|
|
(True, "opaque", True),
|
|
(True, "transparency", True),
|
|
# Default to ignore availability
|
|
(None, "opaque", True),
|
|
(None, "transparency", True),
|
|
],
|
|
)
|
|
async def test_opaque_event(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
mock_calendars_yaml,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
transparency,
|
|
expect_visible_event,
|
|
) -> None:
|
|
"""Test querying the API and fetching events from the server."""
|
|
event = {
|
|
**TEST_EVENT,
|
|
**upcoming(),
|
|
"transparency": transparency,
|
|
}
|
|
mock_events_list_items([event])
|
|
assert await component_setup()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(upcoming_event_url(TEST_YAML_ENTITY))
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert (len(events) > 0) == expect_visible_event
|
|
|
|
# Verify entity state for upcoming event
|
|
state = hass.states.get(TEST_YAML_ENTITY)
|
|
assert state.name == TEST_YAML_ENTITY_NAME
|
|
assert state.state == (STATE_ON if expect_visible_event else STATE_OFF)
|
|
|
|
|
|
@pytest.mark.parametrize("mock_test_setup", [None])
|
|
async def test_scan_calendar_error(
|
|
hass: HomeAssistant,
|
|
component_setup,
|
|
mock_calendars_list,
|
|
config_entry,
|
|
) -> None:
|
|
"""Test that the calendar update handles a server error."""
|
|
mock_calendars_list({}, exc=ClientError())
|
|
assert await component_setup()
|
|
|
|
assert not hass.states.get(TEST_ENTITY)
|
|
|
|
|
|
async def test_future_event_update_behavior(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
) -> None:
|
|
"""Test an future event that becomes active."""
|
|
now = dt_util.now()
|
|
one_hour_from_now = now + datetime.timedelta(minutes=60)
|
|
end_event = one_hour_from_now + datetime.timedelta(minutes=90)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": one_hour_from_now.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
assert await component_setup()
|
|
|
|
# Event has not started yet
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
|
|
# Advance time until event has started
|
|
now += datetime.timedelta(minutes=60)
|
|
freezer.move_to(now)
|
|
async_fire_time_changed(hass, now)
|
|
await hass.async_block_till_done()
|
|
|
|
# Event has started
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.state == STATE_ON
|
|
|
|
|
|
async def test_future_event_offset_update_behavior(
|
|
hass: HomeAssistant,
|
|
freezer: FrozenDateTimeFactory,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
) -> None:
|
|
"""Test an future event that becomes active."""
|
|
now = dt_util.now()
|
|
one_hour_from_now = now + datetime.timedelta(minutes=60)
|
|
end_event = one_hour_from_now + datetime.timedelta(minutes=90)
|
|
event_summary = "Test Event in Progress"
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": one_hour_from_now.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
"summary": f"{event_summary} !!-15",
|
|
}
|
|
mock_events_list_items([event])
|
|
assert await component_setup()
|
|
|
|
# Event has not started yet
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert not state.attributes["offset_reached"]
|
|
|
|
# Advance time until event has started
|
|
now += datetime.timedelta(minutes=45)
|
|
freezer.move_to(now)
|
|
async_fire_time_changed(hass, now)
|
|
await hass.async_block_till_done()
|
|
|
|
# Event has not started, but the offset was reached
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.state == STATE_OFF
|
|
assert state.attributes["offset_reached"]
|
|
|
|
|
|
async def test_unique_id(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
config_entry,
|
|
) -> None:
|
|
"""Test entity is created with a unique id based on the config entry."""
|
|
mock_events_list_items([])
|
|
assert await component_setup()
|
|
|
|
registry_entries = er.async_entries_for_config_entry(
|
|
entity_registry, config_entry.entry_id
|
|
)
|
|
assert {entry.unique_id for entry in registry_entries} == {
|
|
f"{config_entry.unique_id}-{CALENDAR_ID}"
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"old_unique_id", [CALENDAR_ID, f"{CALENDAR_ID}-we_are_we_are_a_test_calendar"]
|
|
)
|
|
async def test_unique_id_migration(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
config_entry,
|
|
old_unique_id,
|
|
) -> None:
|
|
"""Test that old unique id format is migrated to the new format that supports multiple accounts."""
|
|
# Create an entity using the old unique id format
|
|
entity_registry.async_get_or_create(
|
|
DOMAIN,
|
|
Platform.CALENDAR,
|
|
unique_id=old_unique_id,
|
|
config_entry=config_entry,
|
|
)
|
|
registry_entries = er.async_entries_for_config_entry(
|
|
entity_registry, config_entry.entry_id
|
|
)
|
|
assert {entry.unique_id for entry in registry_entries} == {old_unique_id}
|
|
|
|
mock_events_list_items([])
|
|
assert await component_setup()
|
|
|
|
registry_entries = er.async_entries_for_config_entry(
|
|
entity_registry, config_entry.entry_id
|
|
)
|
|
assert {entry.unique_id for entry in registry_entries} == {
|
|
f"{config_entry.unique_id}-{CALENDAR_ID}"
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"calendars_config",
|
|
[
|
|
[
|
|
{
|
|
"cal_id": CALENDAR_ID,
|
|
"entities": [
|
|
{
|
|
"device_id": "backyard_light",
|
|
"name": "Backyard Light",
|
|
"search": "#Backyard",
|
|
},
|
|
{
|
|
"device_id": "front_light",
|
|
"name": "Front Light",
|
|
"search": "#Front",
|
|
},
|
|
],
|
|
}
|
|
],
|
|
],
|
|
)
|
|
async def test_invalid_unique_id_cleanup(
|
|
hass: HomeAssistant,
|
|
entity_registry: er.EntityRegistry,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
config_entry,
|
|
mock_calendars_yaml,
|
|
) -> None:
|
|
"""Test that old unique id format that is not actually unique is removed."""
|
|
# Create an entity using the old unique id format
|
|
entity_registry.async_get_or_create(
|
|
DOMAIN,
|
|
Platform.CALENDAR,
|
|
unique_id=f"{CALENDAR_ID}-backyard_light",
|
|
config_entry=config_entry,
|
|
)
|
|
entity_registry.async_get_or_create(
|
|
DOMAIN,
|
|
Platform.CALENDAR,
|
|
unique_id=f"{CALENDAR_ID}-front_light",
|
|
config_entry=config_entry,
|
|
)
|
|
registry_entries = er.async_entries_for_config_entry(
|
|
entity_registry, config_entry.entry_id
|
|
)
|
|
assert {entry.unique_id for entry in registry_entries} == {
|
|
f"{CALENDAR_ID}-backyard_light",
|
|
f"{CALENDAR_ID}-front_light",
|
|
}
|
|
|
|
mock_events_list_items([])
|
|
assert await component_setup()
|
|
|
|
registry_entries = er.async_entries_for_config_entry(
|
|
entity_registry, config_entry.entry_id
|
|
)
|
|
assert not registry_entries
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("time_zone", "event_order", "calendar_access_role"),
|
|
# This only tests the reader role to force testing against the local
|
|
# database filtering based on start/end time. (free busy reader would
|
|
# just use the API response which this test is not exercising)
|
|
[
|
|
("America/Los_Angeles", ["One", "Two", "All Day Event"], "reader"),
|
|
("America/Regina", ["One", "Two", "All Day Event"], "reader"),
|
|
("UTC", ["One", "All Day Event", "Two"], "reader"),
|
|
("Asia/Tokyo", ["All Day Event", "One", "Two"], "reader"),
|
|
],
|
|
)
|
|
async def test_all_day_iter_order(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
time_zone,
|
|
event_order,
|
|
) -> None:
|
|
"""Test the sort order of an all day events depending on the time zone."""
|
|
hass.config.set_time_zone(time_zone)
|
|
mock_events_list_items(
|
|
[
|
|
{
|
|
**TEST_EVENT,
|
|
"id": "event-id-3",
|
|
"summary": "All Day Event",
|
|
"start": {"date": "2022-10-08"},
|
|
"end": {"date": "2022-10-09"},
|
|
},
|
|
{
|
|
**TEST_EVENT,
|
|
"id": "event-id-1",
|
|
"summary": "One",
|
|
"start": {"dateTime": "2022-10-07T23:00:00+00:00"},
|
|
"end": {"dateTime": "2022-10-07T23:30:00+00:00"},
|
|
},
|
|
{
|
|
**TEST_EVENT,
|
|
"id": "event-id-2",
|
|
"summary": "Two",
|
|
"start": {"dateTime": "2022-10-08T01:00:00+00:00"},
|
|
"end": {"dateTime": "2022-10-08T02:00:00+00:00"},
|
|
},
|
|
]
|
|
)
|
|
assert await component_setup()
|
|
|
|
client = await hass_client()
|
|
response = await client.get(
|
|
get_events_url(TEST_ENTITY, "2022-10-06T00:00:00Z", "2022-10-09T00:00:00Z")
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert [event["summary"] for event in events] == event_order
|
|
|
|
|
|
async def test_websocket_create(
|
|
hass: HomeAssistant,
|
|
component_setup: ComponentSetup,
|
|
test_api_calendar: dict[str, Any],
|
|
mock_insert_event: Callable[[str, dict[str, Any]], None],
|
|
mock_events_list: ApiResult,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
ws_client: ClientFixture,
|
|
) -> None:
|
|
"""Test websocket create command that sets a date/time range."""
|
|
mock_events_list({})
|
|
assert await component_setup()
|
|
|
|
aioclient_mock.clear_requests()
|
|
mock_insert_event(
|
|
calendar_id=CALENDAR_ID,
|
|
)
|
|
|
|
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",
|
|
},
|
|
},
|
|
)
|
|
assert len(aioclient_mock.mock_calls) == 1
|
|
assert aioclient_mock.mock_calls[0][2] == {
|
|
"summary": "Bastille Day Party",
|
|
"description": None,
|
|
"start": {
|
|
"dateTime": "1997-07-14T11:00:00-06:00",
|
|
"timeZone": "America/Regina",
|
|
},
|
|
"end": {"dateTime": "1997-07-14T22:00:00-06:00", "timeZone": "America/Regina"},
|
|
}
|
|
|
|
|
|
async def test_websocket_create_all_day(
|
|
hass: HomeAssistant,
|
|
component_setup: ComponentSetup,
|
|
test_api_calendar: dict[str, Any],
|
|
mock_insert_event: Callable[[str, dict[str, Any]], None],
|
|
mock_events_list: ApiResult,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
ws_client: ClientFixture,
|
|
) -> None:
|
|
"""Test websocket create command for an all day event."""
|
|
mock_events_list({})
|
|
assert await component_setup()
|
|
|
|
aioclient_mock.clear_requests()
|
|
mock_insert_event(
|
|
calendar_id=CALENDAR_ID,
|
|
)
|
|
|
|
client = await ws_client()
|
|
await client.cmd_result(
|
|
"create",
|
|
{
|
|
"entity_id": TEST_ENTITY,
|
|
"event": {
|
|
"summary": "Bastille Day Party",
|
|
"dtstart": "1997-07-14",
|
|
"dtend": "1997-07-15",
|
|
"rrule": "FREQ=YEARLY",
|
|
},
|
|
},
|
|
)
|
|
assert len(aioclient_mock.mock_calls) == 1
|
|
assert aioclient_mock.mock_calls[0][2] == {
|
|
"summary": "Bastille Day Party",
|
|
"description": None,
|
|
"start": {
|
|
"date": "1997-07-14",
|
|
},
|
|
"end": {"date": "1997-07-15"},
|
|
"recurrence": ["RRULE:FREQ=YEARLY"],
|
|
}
|
|
|
|
|
|
async def test_websocket_delete(
|
|
ws_client: ClientFixture,
|
|
hass_client: ClientSessionGenerator,
|
|
component_setup,
|
|
mock_events_list: ApiResult,
|
|
mock_events_list_items: ApiResult,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test websocket delete command."""
|
|
mock_events_list_items(
|
|
[
|
|
{
|
|
**TEST_EVENT,
|
|
"id": "event-id-1",
|
|
"iCalUID": "event-id-1@google.com",
|
|
"summary": "All Day Event",
|
|
"start": {"date": "2022-10-08"},
|
|
"end": {"date": "2022-10-09"},
|
|
},
|
|
]
|
|
)
|
|
assert await component_setup()
|
|
assert len(aioclient_mock.mock_calls) == 2
|
|
|
|
aioclient_mock.clear_requests()
|
|
|
|
# Expect a delete request as well as a follow up to sync state from server
|
|
aioclient_mock.delete(f"{API_BASE_URL}/calendars/{CALENDAR_ID}/events/event-id-1")
|
|
mock_events_list_items([])
|
|
|
|
client = await ws_client()
|
|
await client.cmd_result(
|
|
"delete",
|
|
{
|
|
"entity_id": TEST_ENTITY,
|
|
"uid": "event-id-1@google.com",
|
|
},
|
|
)
|
|
|
|
assert len(aioclient_mock.mock_calls) == 2
|
|
assert aioclient_mock.mock_calls[0][0] == "delete"
|
|
|
|
|
|
async def test_websocket_delete_recurring_event_instance(
|
|
ws_client: ClientFixture,
|
|
hass_client: ClientSessionGenerator,
|
|
component_setup,
|
|
mock_events_list: ApiResult,
|
|
mock_events_list_items: ApiResult,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
) -> None:
|
|
"""Test websocket delete command with recurring events."""
|
|
mock_events_list_items(
|
|
[
|
|
{
|
|
**TEST_EVENT,
|
|
"id": "event-id-1",
|
|
"iCalUID": "event-id-1@google.com",
|
|
"summary": "All Day Event",
|
|
"start": {"date": "2022-10-08"},
|
|
"end": {"date": "2022-10-09"},
|
|
"recurrence": ["RRULE:FREQ=WEEKLY"],
|
|
},
|
|
]
|
|
)
|
|
assert await component_setup()
|
|
assert len(aioclient_mock.mock_calls) == 2
|
|
|
|
# Get a time range for the first event and the second instance of the
|
|
# recurring event.
|
|
web_client = await hass_client()
|
|
response = await web_client.get(
|
|
get_events_url(TEST_ENTITY, "2022-10-06T00:00:00Z", "2022-10-20T00:00:00Z")
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
assert len(events) == 2
|
|
|
|
# Delete the second instance
|
|
event = events[1]
|
|
assert event["uid"] == "event-id-1@google.com"
|
|
assert event["recurrence_id"] == "event-id-1_20221015"
|
|
assert event["rrule"] == "FREQ=WEEKLY"
|
|
|
|
# Expect a delete request as well as a follow up to sync state from server
|
|
aioclient_mock.clear_requests()
|
|
aioclient_mock.patch(
|
|
f"{API_BASE_URL}/calendars/{CALENDAR_ID}/events/event-id-1_20221015"
|
|
)
|
|
mock_events_list_items([])
|
|
|
|
client = await ws_client()
|
|
await client.cmd_result(
|
|
"delete",
|
|
{
|
|
"entity_id": TEST_ENTITY,
|
|
"uid": event["uid"],
|
|
"recurrence_id": event["recurrence_id"],
|
|
},
|
|
)
|
|
|
|
assert len(aioclient_mock.mock_calls) == 2
|
|
assert aioclient_mock.mock_calls[0][0] == "patch"
|
|
# Request to cancel the second instance of the recurring event
|
|
assert aioclient_mock.mock_calls[0][2] == {
|
|
"id": "event-id-1_20221015",
|
|
"status": "cancelled",
|
|
}
|
|
|
|
# Attempt delete again, but this time for all future instances
|
|
aioclient_mock.clear_requests()
|
|
aioclient_mock.patch(f"{API_BASE_URL}/calendars/{CALENDAR_ID}/events/event-id-1")
|
|
mock_events_list_items([])
|
|
|
|
client = await ws_client()
|
|
await client.cmd_result(
|
|
"delete",
|
|
{
|
|
"entity_id": TEST_ENTITY,
|
|
"uid": event["uid"],
|
|
"recurrence_id": event["recurrence_id"],
|
|
"recurrence_range": "THISANDFUTURE",
|
|
},
|
|
)
|
|
|
|
assert len(aioclient_mock.mock_calls) == 2
|
|
assert aioclient_mock.mock_calls[0][0] == "patch"
|
|
# Request to cancel all events after the second instance
|
|
assert aioclient_mock.mock_calls[0][2] == {
|
|
"id": "event-id-1",
|
|
"recurrence": ["RRULE:FREQ=WEEKLY;UNTIL=20221014"],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("calendar_access_role", "token_scopes", "config_entry_options"),
|
|
[
|
|
(
|
|
"reader",
|
|
["https://www.googleapis.com/auth/calendar"],
|
|
{CONF_CALENDAR_ACCESS: "read_write"},
|
|
),
|
|
(
|
|
"reader",
|
|
["https://www.googleapis.com/auth/calendar.readonly"],
|
|
{CONF_CALENDAR_ACCESS: "read_only"},
|
|
),
|
|
(
|
|
"owner",
|
|
["https://www.googleapis.com/auth/calendar.readonly"],
|
|
{CONF_CALENDAR_ACCESS: "read_only"},
|
|
),
|
|
],
|
|
)
|
|
async def test_readonly_websocket_create(
|
|
hass: HomeAssistant,
|
|
component_setup: ComponentSetup,
|
|
test_api_calendar: dict[str, Any],
|
|
mock_insert_event: Callable[[str, dict[str, Any]], None],
|
|
mock_events_list: ApiResult,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
ws_client: ClientFixture,
|
|
) -> None:
|
|
"""Test websocket create command with read only access."""
|
|
mock_events_list({})
|
|
assert await component_setup()
|
|
|
|
aioclient_mock.clear_requests()
|
|
mock_insert_event(
|
|
calendar_id=CALENDAR_ID,
|
|
)
|
|
|
|
client = await ws_client()
|
|
result = await client.cmd(
|
|
"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",
|
|
},
|
|
},
|
|
)
|
|
assert result.get("error")
|
|
assert result["error"].get("code") == "not_supported"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
"calendars_config",
|
|
[
|
|
[
|
|
{
|
|
"cal_id": CALENDAR_ID,
|
|
"entities": [
|
|
{
|
|
"device_id": "backyard_light",
|
|
"name": "Backyard Light",
|
|
"search": "#Backyard",
|
|
},
|
|
],
|
|
}
|
|
],
|
|
],
|
|
)
|
|
async def test_readonly_search_calendar(
|
|
hass: HomeAssistant,
|
|
component_setup: ComponentSetup,
|
|
mock_calendars_yaml,
|
|
mock_insert_event: Callable[[str, dict[str, Any]], None],
|
|
mock_events_list: ApiResult,
|
|
aioclient_mock: AiohttpClientMocker,
|
|
ws_client: ClientFixture,
|
|
) -> None:
|
|
"""Test calendar configured with yaml/search does not support mutation."""
|
|
mock_events_list({})
|
|
assert await component_setup()
|
|
|
|
aioclient_mock.clear_requests()
|
|
mock_insert_event(
|
|
calendar_id=CALENDAR_ID,
|
|
)
|
|
|
|
client = await ws_client()
|
|
result = await client.cmd(
|
|
"create",
|
|
{
|
|
"entity_id": TEST_YAML_ENTITY,
|
|
"event": {
|
|
"summary": "Bastille Day Party",
|
|
"dtstart": "1997-07-14T17:00:00+00:00",
|
|
"dtend": "1997-07-15T04:00:00+00:00",
|
|
},
|
|
},
|
|
)
|
|
assert result.get("error")
|
|
assert result["error"].get("code") == "not_supported"
|
|
|
|
|
|
@pytest.mark.parametrize("calendar_access_role", ["reader", "freeBusyReader"])
|
|
async def test_all_day_reader_access(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test that reader / freebusy reader access can load properly."""
|
|
week_from_today = dt_util.now().date() + datetime.timedelta(days=7)
|
|
end_event = week_from_today + datetime.timedelta(days=1)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": week_from_today.isoformat()},
|
|
"end": {"date": end_event.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": True,
|
|
"offset_reached": False,
|
|
"start_time": week_from_today.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
@pytest.mark.parametrize("calendar_access_role", ["reader", "freeBusyReader"])
|
|
async def test_reader_in_progress_event(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test reader access for an event in process."""
|
|
middle_of_event = dt_util.now() - datetime.timedelta(minutes=30)
|
|
end_event = middle_of_event + datetime.timedelta(minutes=60)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": middle_of_event.isoformat()},
|
|
"end": {"dateTime": end_event.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_ON
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": middle_of_event.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
}
|
|
|
|
|
|
async def test_all_day_event_without_duration(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test that an all day event without a duration is adjusted to have a duration of one day."""
|
|
week_from_today = dt_util.now().date() + datetime.timedelta(days=7)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": week_from_today.isoformat()},
|
|
"end": {"date": week_from_today.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
expected_end_event = week_from_today + datetime.timedelta(days=1)
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": True,
|
|
"offset_reached": False,
|
|
"start_time": week_from_today.strftime(DATE_STR_FORMAT),
|
|
"end_time": expected_end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
async def test_event_without_duration(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Google calendar UI allows creating events without a duration."""
|
|
one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"dateTime": one_hour_from_now.isoformat()},
|
|
"end": {"dateTime": one_hour_from_now.isoformat()},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
# Confirm the event is parsed successfully, but we don't assert on the
|
|
# specific end date as the client library may adjust it
|
|
assert state.attributes.get("message") == event["summary"]
|
|
assert state.attributes.get("start_time") == one_hour_from_now.strftime(
|
|
DATE_STR_FORMAT
|
|
)
|
|
|
|
|
|
async def test_event_differs_timezone(
|
|
hass: HomeAssistant, mock_events_list_items, component_setup
|
|
) -> None:
|
|
"""Test a case where the event has a different start/end timezone."""
|
|
one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
|
|
end_event = one_hour_from_now + datetime.timedelta(hours=8)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {
|
|
"dateTime": one_hour_from_now.isoformat(),
|
|
"timeZone": "America/Regina",
|
|
},
|
|
"end": {"dateTime": end_event.isoformat(), "timeZone": "UTC"},
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
assert dict(state.attributes) == {
|
|
"friendly_name": TEST_ENTITY_NAME,
|
|
"message": event["summary"],
|
|
"all_day": False,
|
|
"offset_reached": False,
|
|
"start_time": one_hour_from_now.strftime(DATE_STR_FORMAT),
|
|
"end_time": end_event.strftime(DATE_STR_FORMAT),
|
|
"location": event["location"],
|
|
"description": event["description"],
|
|
"supported_features": 3,
|
|
}
|
|
|
|
|
|
@pytest.mark.freeze_time("2023-11-30 12:15:00 +00:00")
|
|
async def test_invalid_rrule_fix(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
mock_events_list_items,
|
|
component_setup,
|
|
) -> None:
|
|
"""Test that an invalid RRULE returned from Google Calendar API is handled correctly end to end."""
|
|
week_from_today = dt_util.now().date() + datetime.timedelta(days=7)
|
|
end_event = week_from_today + datetime.timedelta(days=1)
|
|
event = {
|
|
**TEST_EVENT,
|
|
"start": {"date": week_from_today.isoformat()},
|
|
"end": {"date": end_event.isoformat()},
|
|
"recurrence": [
|
|
"RRULE:DATE;TZID=Europe/Warsaw:20230818T020000,20230915T020000,20231013T020000,20231110T010000,20231208T010000",
|
|
],
|
|
}
|
|
mock_events_list_items([event])
|
|
|
|
assert await component_setup()
|
|
|
|
state = hass.states.get(TEST_ENTITY)
|
|
assert state.name == TEST_ENTITY_NAME
|
|
assert state.state == STATE_OFF
|
|
|
|
# Pick a date range that contains two instances of the event
|
|
web_client = await hass_client()
|
|
response = await web_client.get(
|
|
get_events_url(TEST_ENTITY, "2023-08-10T00:00:00Z", "2023-09-20T00:00:00Z")
|
|
)
|
|
assert response.status == HTTPStatus.OK
|
|
events = await response.json()
|
|
|
|
# Both instances are returned, however the RDATE rule is ignored by Home
|
|
# Assistant so they are just treateded as flattened events.
|
|
assert len(events) == 2
|
|
|
|
event = events[0]
|
|
assert event["uid"] == "cydrevtfuybguinhomj@google.com"
|
|
assert event["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230818"
|
|
assert event["rrule"] is None
|
|
|
|
event = events[1]
|
|
assert event["uid"] == "cydrevtfuybguinhomj@google.com"
|
|
assert event["recurrence_id"] == "_c8rinwq863h45qnucyoi43ny8_20230915"
|
|
assert event["rrule"] is None
|