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:
parent
e2308fd15c
commit
5d1ca73a34
5 changed files with 456 additions and 24 deletions
|
@ -8,7 +8,12 @@ from datetime import datetime, timedelta
|
|||
import logging
|
||||
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.model import AccessRole, DateOrDatetime, Event
|
||||
from gcal_sync.store import ScopedCalendarStore
|
||||
|
@ -18,7 +23,13 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant.components.calendar import (
|
||||
ENTITY_ID_FORMAT,
|
||||
EVENT_DESCRIPTION,
|
||||
EVENT_END,
|
||||
EVENT_RRULE,
|
||||
EVENT_START,
|
||||
EVENT_SUMMARY,
|
||||
CalendarEntity,
|
||||
CalendarEntityFeature,
|
||||
CalendarEvent,
|
||||
extract_offset,
|
||||
is_offset_reached,
|
||||
|
@ -52,11 +63,9 @@ from . import (
|
|||
load_config,
|
||||
update_config,
|
||||
)
|
||||
from .api import get_feature_access
|
||||
from .const import (
|
||||
DATA_SERVICE,
|
||||
DATA_STORE,
|
||||
EVENT_DESCRIPTION,
|
||||
EVENT_END_DATE,
|
||||
EVENT_END_DATETIME,
|
||||
EVENT_IN,
|
||||
|
@ -64,9 +73,7 @@ from .const import (
|
|||
EVENT_IN_WEEKS,
|
||||
EVENT_START_DATE,
|
||||
EVENT_START_DATETIME,
|
||||
EVENT_SUMMARY,
|
||||
EVENT_TYPES_CONF,
|
||||
FeatureAccess,
|
||||
)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
@ -235,6 +242,7 @@ async def async_setup_entry(
|
|||
generate_entity_id(ENTITY_ID_FORMAT, entity_name, hass=hass),
|
||||
unique_id,
|
||||
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)
|
||||
|
||||
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(
|
||||
SERVICE_CREATE_EVENT,
|
||||
CREATE_EVENT_SCHEMA,
|
||||
|
@ -382,6 +390,7 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
|||
entity_id: str,
|
||||
unique_id: str | None,
|
||||
entity_enabled: bool,
|
||||
supports_write: bool,
|
||||
) -> None:
|
||||
"""Create the Calendar event device."""
|
||||
super().__init__(coordinator)
|
||||
|
@ -395,6 +404,10 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
|||
self.entity_id = entity_id
|
||||
self._attr_unique_id = unique_id
|
||||
self._attr_entity_registry_enabled_default = entity_enabled
|
||||
if supports_write:
|
||||
self._attr_supported_features = (
|
||||
CalendarEntityFeature.CREATE_EVENT | CalendarEntityFeature.DELETE_EVENT
|
||||
)
|
||||
|
||||
@property
|
||||
def should_poll(self) -> bool:
|
||||
|
@ -486,10 +499,62 @@ class GoogleCalendarEntity(CoordinatorEntity, CalendarEntity):
|
|||
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:
|
||||
"""Return a CalendarEvent from an API event."""
|
||||
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,
|
||||
start=event.start.value,
|
||||
end=event.end.value,
|
||||
|
|
|
@ -64,7 +64,7 @@ CLIENT_SECRET = "client-secret"
|
|||
@pytest.fixture(name="calendar_access_role")
|
||||
def test_calendar_access_role() -> str:
|
||||
"""Default access role to use for test_api_calendar in tests."""
|
||||
return "reader"
|
||||
return "owner"
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
@ -2,17 +2,21 @@
|
|||
|
||||
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 import ClientWebSocketResponse
|
||||
from aiohttp.client_exceptions import ClientError
|
||||
from gcal_sync.auth import API_BASE_URL
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.google.const import 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
|
||||
|
@ -23,9 +27,12 @@ from .conftest import (
|
|||
TEST_API_ENTITY_NAME,
|
||||
TEST_YAML_ENTITY,
|
||||
TEST_YAML_ENTITY_NAME,
|
||||
ApiResult,
|
||||
ComponentSetup,
|
||||
)
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
from tests.test_util.aiohttp import AiohttpClientMocker
|
||||
|
||||
TEST_ENTITY = TEST_API_ENTITY
|
||||
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)
|
||||
def mock_test_setup(
|
||||
test_api_calendar,
|
||||
|
@ -99,8 +98,55 @@ def upcoming_event_url(entity: str = TEST_ENTITY) -> str:
|
|||
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):
|
||||
"""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)
|
||||
end_event = week_from_today + datetime.timedelta(days=1)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
end_event = one_hour_from_now + datetime.timedelta(minutes=60)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
end_event = middle_of_event + datetime.timedelta(minutes=60)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
end_event = middle_of_event + datetime.timedelta(minutes=60)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
async def test_all_day_offset_in_progress_event(
|
||||
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)
|
||||
end_event = tomorrow + datetime.timedelta(days=1)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
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()
|
||||
day_after_tomorrow = now.date() + datetime.timedelta(days=2)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
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)
|
||||
end_event = start_event + datetime.timedelta(minutes=60)
|
||||
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),
|
||||
"location": event["location"],
|
||||
"description": event["description"],
|
||||
"supported_features": 3,
|
||||
}
|
||||
|
||||
|
||||
|
@ -779,3 +832,317 @@ async def test_all_day_iter_order(
|
|||
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": ["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"],
|
||||
}
|
||||
|
|
|
@ -104,7 +104,7 @@ async def primary_calendar(
|
|||
"""Fixture to return the primary calendar."""
|
||||
mock_calendar_get(
|
||||
"primary",
|
||||
{"id": primary_calendar_email, "summary": "Personal"},
|
||||
{"id": primary_calendar_email, "summary": "Personal", "accessRole": "owner"},
|
||||
exc=primary_calendar_error,
|
||||
)
|
||||
|
||||
|
|
|
@ -768,7 +768,7 @@ async def test_assign_unique_id(
|
|||
|
||||
mock_calendar_get(
|
||||
"primary",
|
||||
{"id": EMAIL_ADDRESS, "summary": "Personal"},
|
||||
{"id": EMAIL_ADDRESS, "summary": "Personal", "accessRole": "owner"},
|
||||
)
|
||||
|
||||
mock_calendars_list({"items": [test_api_calendar]})
|
||||
|
|
Loading…
Add table
Reference in a new issue