From d99b9f7a700fa8298080bc3f7402d7289d7eb99f Mon Sep 17 00:00:00 2001 From: Allen Porter Date: Fri, 8 Mar 2024 19:28:04 -0800 Subject: [PATCH] Fix local calendar handling of empty recurrence ids (#112745) * Fix handling of empty recurrence ids * Revert logging changes --- homeassistant/components/calendar/__init__.py | 13 ++- .../local_calendar/test_calendar.py | 92 +++++++++++++++++++ 2 files changed, 103 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/calendar/__init__.py b/homeassistant/components/calendar/__init__.py index bef0e2fc09f..670d448a430 100644 --- a/homeassistant/components/calendar/__init__.py +++ b/homeassistant/components/calendar/__init__.py @@ -189,6 +189,11 @@ def _validate_rrule(value: Any) -> str: return str(value) +def _empty_as_none(value: str | None) -> str | None: + """Convert any empty string values to None.""" + return value or None + + CREATE_EVENT_SERVICE = "create_event" CREATE_EVENT_SCHEMA = vol.All( cv.has_at_least_one_key(EVENT_START_DATE, EVENT_START_DATETIME, EVENT_IN), @@ -733,7 +738,9 @@ async def handle_calendar_event_create( vol.Required("type"): "calendar/event/delete", vol.Required("entity_id"): cv.entity_id, vol.Required(EVENT_UID): cv.string, - vol.Optional(EVENT_RECURRENCE_ID): cv.string, + vol.Optional(EVENT_RECURRENCE_ID): vol.Any( + vol.All(cv.string, _empty_as_none), None + ), vol.Optional(EVENT_RECURRENCE_RANGE): cv.string, } ) @@ -777,7 +784,9 @@ async def handle_calendar_event_delete( vol.Required("type"): "calendar/event/update", vol.Required("entity_id"): cv.entity_id, vol.Required(EVENT_UID): cv.string, - vol.Optional(EVENT_RECURRENCE_ID): cv.string, + vol.Optional(EVENT_RECURRENCE_ID): vol.Any( + vol.All(cv.string, _empty_as_none), None + ), vol.Optional(EVENT_RECURRENCE_RANGE): cv.string, vol.Required(CONF_EVENT): WEBSOCKET_EVENT_SCHEMA, } diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index 060ea114973..2fa0063dfd8 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -408,6 +408,46 @@ async def test_websocket_delete_recurring( ] +async def test_websocket_delete_empty_recurrence_id( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +) -> None: + """Test websocket delete command with an empty recurrence id no-op.""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-14T17:00:00+00:00", + "dtend": "1997-07-15T04:00:00+00:00", + }, + }, + ) + + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + uid = events[0]["uid"] + + # Delete the event with an empty recurrence id + await client.cmd_result( + "delete", + { + "entity_id": TEST_ENTITY, + "uid": uid, + "recurrence_id": "", + }, + ) + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [] + + async def test_websocket_update( ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn ) -> None: @@ -458,6 +498,58 @@ async def test_websocket_update( ] +async def test_websocket_update_empty_recurrence( + ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn +) -> None: + """Test an edit with an empty recurrence id (no-op).""" + client = await ws_client() + await client.cmd_result( + "create", + { + "entity_id": TEST_ENTITY, + "event": { + "summary": "Bastille Day Party", + "dtstart": "1997-07-14T17:00:00+00:00", + "dtend": "1997-07-15T04:00:00+00:00", + }, + }, + ) + + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T22:00:00-06:00"}, + } + ] + uid = events[0]["uid"] + + # Update the event with an empty string for the recurrence id which should + # have no effect. + await client.cmd_result( + "update", + { + "entity_id": TEST_ENTITY, + "uid": uid, + "recurrence_id": "", + "event": { + "summary": "Bastille Day Party [To be rescheduled]", + "dtstart": "1997-07-15T11:00:00-06:00", + "dtend": "1997-07-15T22:00:00-06:00", + }, + }, + ) + events = await get_events("1997-07-14T00:00:00", "1997-07-16T00:00:00") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party [To be rescheduled]", + "start": {"dateTime": "1997-07-15T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-15T22:00:00-06:00"}, + } + ] + + async def test_websocket_update_recurring_this_and_future( ws_client: ClientFixture, setup_integration: None, get_events: GetEventsFn ) -> None: