Add create and delete for Google Calendar events (#83034)

* Add Google Calendar create/delete support

Includes editing for recurring events

* Fix default calendar access role

* Formatting improvements

* Address other details that have changed due to local sync

* Update tests/components/google/test_calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/google/test_calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Update tests/components/google/test_calendar.py

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Increase test coverage

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Allen Porter 2022-12-01 12:39:58 -08:00 committed by GitHub
parent e2308fd15c
commit 5d1ca73a34
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 456 additions and 24 deletions

View File

@ -8,7 +8,12 @@ from datetime import datetime, timedelta
import logging import logging
from typing import Any from typing import Any
from gcal_sync.api import GoogleCalendarService, ListEventsRequest, SyncEventsRequest from gcal_sync.api import (
GoogleCalendarService,
ListEventsRequest,
Range,
SyncEventsRequest,
)
from gcal_sync.exceptions import ApiException from gcal_sync.exceptions import ApiException
from gcal_sync.model import AccessRole, DateOrDatetime, Event from gcal_sync.model import AccessRole, DateOrDatetime, Event
from gcal_sync.store import ScopedCalendarStore from gcal_sync.store import ScopedCalendarStore
@ -18,7 +23,13 @@ import voluptuous as vol
from homeassistant.components.calendar import ( from homeassistant.components.calendar import (
ENTITY_ID_FORMAT, ENTITY_ID_FORMAT,
EVENT_DESCRIPTION,
EVENT_END,
EVENT_RRULE,
EVENT_START,
EVENT_SUMMARY,
CalendarEntity, CalendarEntity,
CalendarEntityFeature,
CalendarEvent, CalendarEvent,
extract_offset, extract_offset,
is_offset_reached, is_offset_reached,
@ -52,11 +63,9 @@ from . import (
load_config, load_config,
update_config, update_config,
) )
from .api import get_feature_access
from .const import ( from .const import (
DATA_SERVICE, DATA_SERVICE,
DATA_STORE, DATA_STORE,
EVENT_DESCRIPTION,
EVENT_END_DATE, EVENT_END_DATE,
EVENT_END_DATETIME, EVENT_END_DATETIME,
EVENT_IN, EVENT_IN,
@ -64,9 +73,7 @@ from .const import (
EVENT_IN_WEEKS, EVENT_IN_WEEKS,
EVENT_START_DATE, EVENT_START_DATE,
EVENT_START_DATETIME, EVENT_START_DATETIME,
EVENT_SUMMARY,
EVENT_TYPES_CONF, EVENT_TYPES_CONF,
FeatureAccess,
) )
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -235,6 +242,7 @@ async def async_setup_entry(
generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass), generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass),
unique_id, unique_id,
entity_enabled, entity_enabled,
calendar_item.access_role.is_writer,
) )
) )
@ -250,7 +258,7 @@ async def async_setup_entry(
await hass.async_add_executor_job(append_calendars_to_config) await hass.async_add_executor_job(append_calendars_to_config)
platform = entity_platform.async_get_current_platform() platform = entity_platform.async_get_current_platform()
if get_feature_access(hass, config_entry) is FeatureAccess.read_write: if any(calendar_item.access_role.is_writer for calendar_item in result.items):
platform.async_register_entity_service( platform.async_register_entity_service(
SERVICE_CREATE_EVENT, SERVICE_CREATE_EVENT,
CREATE_EVENT_SCHEMA, CREATE_EVENT_SCHEMA,
@ -382,6 +390,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
entity_id: str, entity_id: str,
unique_id: str | None, unique_id: str | None,
entity_enabled: bool, entity_enabled: bool,
supports_write: bool,
) -> None: ) -> None:
"""Create the Calendar event device.""" """Create the Calendar event device."""
super().__init__(coordinator) super().__init__(coordinator)
@ -395,6 +404,10 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
self.entity_id = entity_id self.entity_id = entity_id
self._attr_unique_id = unique_id self._attr_unique_id = unique_id
self._attr_entity_registry_enabled_default = entity_enabled self._attr_entity_registry_enabled_default = entity_enabled
if supports_write:
self._attr_supported_features = (
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
)
@property @property
def should_poll(self) -> bool: def should_poll(self) -> bool:
@ -486,10 +499,62 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
started, handled by CalendarEntity parent class. started, handled by CalendarEntity parent class.
""" """
async def async_create_event(self, **kwargs: Any) -> None:
"""Add a new event to calendar."""
dtstart = kwargs[EVENT_START]
dtend = kwargs[EVENT_END]
start: DateOrDatetime
end: DateOrDatetime
if isinstance(dtstart, datetime):
start = DateOrDatetime(
date_time=dt_util.as_local(dtstart),
timezone=str(dt_util.DEFAULT_TIME_ZONE),
)
end = DateOrDatetime(
date_time=dt_util.as_local(dtend),
timezone=str(dt_util.DEFAULT_TIME_ZONE),
)
else:
start = DateOrDatetime(date=dtstart)
end = DateOrDatetime(date=dtend)
event = Event.parse_obj(
{
EVENT_SUMMARY: kwargs[EVENT_SUMMARY],
"start": start,
"end": end,
EVENT_DESCRIPTION: kwargs.get(EVENT_DESCRIPTION),
}
)
if rrule := kwargs.get(EVENT_RRULE):
event.recurrence = [rrule]
await self.coordinator.sync.store_service.async_add_event(event)
await self.coordinator.async_refresh()
async def async_delete_event(
self,
uid: str,
recurrence_id: str | None = None,
recurrence_range: str | None = None,
) -> None:
"""Delete an event on the calendar."""
range_value: Range = Range.NONE
if recurrence_range == Range.THIS_AND_FUTURE:
range_value = Range.THIS_AND_FUTURE
await self.coordinator.sync.store_service.async_delete_event(
ical_uuid=uid,
event_id=recurrence_id,
recurrence_range=range_value,
)
await self.coordinator.async_refresh()
def _get_calendar_event(event: Event) -> CalendarEvent: def _get_calendar_event(event: Event) -> CalendarEvent:
"""Return a CalendarEvent from an API event.""" """Return a CalendarEvent from an API event."""
return CalendarEvent( return CalendarEvent(
uid=event.ical_uuid,
recurrence_id=event.id if event.recurring_event_id else None,
rrule=event.recurrence[0] if len(event.recurrence) == 1 else None,
summary=event.summary, summary=event.summary,
start=event.start.value, start=event.start.value,
end=event.end.value, end=event.end.value,

View File

@ -64,7 +64,7 @@ CLIENT_SECRET = "client-secret"
@pytest.fixture(name="calendar_access_role") @pytest.fixture(name="calendar_access_role")
def test_calendar_access_role() -> str: def test_calendar_access_role() -> str:
"""Default access role to use for test_api_calendar in tests.""" """Default access role to use for test_api_calendar in tests."""
return "reader" return "owner"
@pytest.fixture @pytest.fixture

View File

@ -2,17 +2,21 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Awaitable, Callable
import datetime import datetime
from http import HTTPStatus from http import HTTPStatus
from typing import Any from typing import Any
from unittest.mock import patch from unittest.mock import patch
import urllib import urllib
from aiohttp import ClientWebSocketResponse
from aiohttp.client_exceptions import ClientError from aiohttp.client_exceptions import ClientError
from gcal_sync.auth import API_BASE_URL
import pytest import pytest
from homeassistant.components.google.const import DOMAIN from homeassistant.components.google.const import DOMAIN
from homeassistant.const import STATE_OFF, STATE_ON, Platform 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 import entity_registry as er
from homeassistant.helpers.template import DATE_STR_FORMAT from homeassistant.helpers.template import DATE_STR_FORMAT
import homeassistant.util.dt as dt_util import homeassistant.util.dt as dt_util
@ -23,9 +27,12 @@ from .conftest import (
TEST_API_ENTITY_NAME, TEST_API_ENTITY_NAME,
TEST_YAML_ENTITY, TEST_YAML_ENTITY,
TEST_YAML_ENTITY_NAME, TEST_YAML_ENTITY_NAME,
ApiResult,
ComponentSetup,
) )
from tests.common import async_fire_time_changed from tests.common import async_fire_time_changed
from tests.test_util.aiohttp import AiohttpClientMocker
TEST_ENTITY = TEST_API_ENTITY TEST_ENTITY = TEST_API_ENTITY
TEST_ENTITY_NAME = TEST_API_ENTITY_NAME TEST_ENTITY_NAME = TEST_API_ENTITY_NAME
@ -60,14 +67,6 @@ TEST_EVENT = {
} }
@pytest.fixture(
autouse=True, scope="module", params=["reader", "owner", "freeBusyReader"]
)
def calendar_access_role(request) -> str:
"""Fixture to exercise access roles in tests."""
return request.param
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
def mock_test_setup( def mock_test_setup(
test_api_calendar, test_api_calendar,
@ -99,8 +98,55 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str:
return get_events_url(entity, start, end) 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: Callable[[HomeAssistant], Awaitable[ClientWebSocketResponse]],
) -> 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, mock_events_list_items, component_setup): async def test_all_day_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test for an all day calendar event."""
week_from_today = dt_util.now().date() + datetime.timedelta(days=7) week_from_today = dt_util.now().date() + datetime.timedelta(days=7)
end_event = week_from_today + datetime.timedelta(days=1) end_event = week_from_today + datetime.timedelta(days=1)
event = { event = {
@ -124,11 +170,12 @@ async def test_all_day_event(hass, mock_events_list_items, component_setup):
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
async def test_future_event(hass, mock_events_list_items, component_setup): async def test_future_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test for an upcoming event."""
one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30) one_hour_from_now = dt_util.now() + datetime.timedelta(minutes=30)
end_event = one_hour_from_now + datetime.timedelta(minutes=60) end_event = one_hour_from_now + datetime.timedelta(minutes=60)
event = { event = {
@ -152,11 +199,12 @@ async def test_future_event(hass, mock_events_list_items, component_setup):
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
async def test_in_progress_event(hass, mock_events_list_items, component_setup): async def test_in_progress_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test an event that is active now."""
middle_of_event = dt_util.now() - datetime.timedelta(minutes=30) middle_of_event = dt_util.now() - datetime.timedelta(minutes=30)
end_event = middle_of_event + datetime.timedelta(minutes=60) end_event = middle_of_event + datetime.timedelta(minutes=60)
event = { event = {
@ -180,11 +228,12 @@ async def test_in_progress_event(hass, mock_events_list_items, component_setup):
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
async def test_offset_in_progress_event(hass, mock_events_list_items, component_setup): async def test_offset_in_progress_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test an event that is active now with an offset."""
middle_of_event = dt_util.now() + datetime.timedelta(minutes=14) middle_of_event = dt_util.now() + datetime.timedelta(minutes=14)
end_event = middle_of_event + datetime.timedelta(minutes=60) end_event = middle_of_event + datetime.timedelta(minutes=60)
event_summary = "Test Event in Progress" event_summary = "Test Event in Progress"
@ -210,13 +259,14 @@ async def test_offset_in_progress_event(hass, mock_events_list_items, component_
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
async def test_all_day_offset_in_progress_event( async def test_all_day_offset_in_progress_event(
hass, mock_events_list_items, component_setup hass, mock_events_list_items, component_setup
): ):
"""Test that we can create an event trigger on device.""" """Test an all day event that is currently in progress due to an offset."""
tomorrow = dt_util.now().date() + datetime.timedelta(days=1) tomorrow = dt_util.now().date() + datetime.timedelta(days=1)
end_event = tomorrow + datetime.timedelta(days=1) end_event = tomorrow + datetime.timedelta(days=1)
event_summary = "Test All Day Event Offset In Progress" event_summary = "Test All Day Event Offset In Progress"
@ -242,11 +292,12 @@ async def test_all_day_offset_in_progress_event(
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
async def test_all_day_offset_event(hass, mock_events_list_items, component_setup): async def test_all_day_offset_event(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test an all day event that not in progress due to an offset."""
now = dt_util.now() now = dt_util.now()
day_after_tomorrow = now.date() + datetime.timedelta(days=2) day_after_tomorrow = now.date() + datetime.timedelta(days=2)
end_event = day_after_tomorrow + datetime.timedelta(days=1) end_event = day_after_tomorrow + datetime.timedelta(days=1)
@ -274,11 +325,12 @@ async def test_all_day_offset_event(hass, mock_events_list_items, component_setu
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
async def test_missing_summary(hass, mock_events_list_items, component_setup): async def test_missing_summary(hass, mock_events_list_items, component_setup):
"""Test that we can create an event trigger on device.""" """Test that a summary is optional."""
start_event = dt_util.now() + datetime.timedelta(minutes=14) start_event = dt_util.now() + datetime.timedelta(minutes=14)
end_event = start_event + datetime.timedelta(minutes=60) end_event = start_event + datetime.timedelta(minutes=60)
event = { event = {
@ -303,6 +355,7 @@ async def test_missing_summary(hass, mock_events_list_items, component_setup):
"end_time": end_event.strftime(DATE_STR_FORMAT), "end_time": end_event.strftime(DATE_STR_FORMAT),
"location": event["location"], "location": event["location"],
"description": event["description"], "description": event["description"],
"supported_features": 3,
} }
@ -779,3 +832,317 @@ async def test_all_day_iter_order(
assert response.status == HTTPStatus.OK assert response.status == HTTPStatus.OK
events = await response.json() events = await response.json()
assert [event["summary"] for event in events] == event_order 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": ["FREQ=YEARLY"],
}
async def test_websocket_delete(
ws_client: ClientFixture,
hass_client,
component_setup,
mock_events_list: ApiResult,
mock_events_list_items: ApiResult,
aioclient_mock,
):
"""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,
component_setup,
mock_events_list: ApiResult,
mock_events_list_items: ApiResult,
aioclient_mock,
):
"""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"
# 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=20221015"],
}
@pytest.mark.parametrize(
"calendar_access_role",
["reader"],
)
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("calendar_access_role", ["reader", "freeBusyReader"])
async def test_all_day_reader_access(hass, mock_events_list_items, component_setup):
"""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, mock_events_list_items, component_setup):
"""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"],
}

View File

@ -104,7 +104,7 @@ async def primary_calendar(
"""Fixture to return the primary calendar.""" """Fixture to return the primary calendar."""
mock_calendar_get( mock_calendar_get(
"primary", "primary",
{"id": primary_calendar_email, "summary": "Personal"}, {"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"},
exc=primary_calendar_error, exc=primary_calendar_error,
) )

View File

@ -768,7 +768,7 @@ async def test_assign_unique_id(
mock_calendar_get( mock_calendar_get(
"primary", "primary",
{"id": EMAIL_ADDRESS, "summary": "Personal"}, {"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "owner"},
) )
mock_calendars_list({"items": [test_api_calendar]}) mock_calendars_list({"items": [test_api_calendar]})