diff --git a/homeassistant/components/local_calendar/calendar.py b/homeassistant/components/local_calendar/calendar.py index 4b6d9444fd8..6cfcaec61d0 100644 --- a/homeassistant/components/local_calendar/calendar.py +++ b/homeassistant/components/local_calendar/calendar.py @@ -2,7 +2,7 @@ from __future__ import annotations -from datetime import datetime +from datetime import date, datetime, timedelta import logging from typing import Any @@ -186,14 +186,23 @@ def _parse_event(event: dict[str, Any]) -> Event: def _get_calendar_event(event: Event) -> CalendarEvent: """Return a CalendarEvent from an API event.""" + start: datetime | date + end: datetime | date + if isinstance(event.start, datetime) and isinstance(event.end, datetime): + start = dt_util.as_local(event.start) + end = dt_util.as_local(event.end) + if (end - start) <= timedelta(seconds=0): + end = start + timedelta(minutes=30) + else: + start = event.start + end = event.end + if (end - start) <= timedelta(days=0): + end = start + timedelta(days=1) + return CalendarEvent( summary=event.summary, - start=dt_util.as_local(event.start) - if isinstance(event.start, datetime) - else event.start, - end=dt_util.as_local(event.end) - if isinstance(event.end, datetime) - else event.end, + start=start, + end=end, description=event.description, uid=event.uid, rrule=event.rrule.as_rrule_str() if event.rrule else None, diff --git a/tests/components/local_calendar/conftest.py b/tests/components/local_calendar/conftest.py index b083bbac78a..7dc294087bd 100644 --- a/tests/components/local_calendar/conftest.py +++ b/tests/components/local_calendar/conftest.py @@ -26,10 +26,10 @@ TEST_ENTITY = "calendar.light_schedule" class FakeStore(LocalCalendarStore): """Mock storage implementation.""" - def __init__(self, hass: HomeAssistant, path: Path) -> None: + def __init__(self, hass: HomeAssistant, path: Path, ics_content: str) -> None: """Initialize FakeStore.""" super().__init__(hass, path) - self._content = "" + self._content = ics_content def _load(self) -> str: """Read from calendar storage.""" @@ -40,15 +40,21 @@ class FakeStore(LocalCalendarStore): self._content = ics_content +@pytest.fixture(name="ics_content", autouse=True) +def mock_ics_content() -> str: + """Fixture to allow tests to set initial ics content for the calendar store.""" + return "" + + @pytest.fixture(name="store", autouse=True) -def mock_store() -> Generator[None, None, None]: +def mock_store(ics_content: str) -> Generator[None, None, None]: """Test cleanup, remove any media storage persisted during the test.""" stores: dict[Path, FakeStore] = {} def new_store(hass: HomeAssistant, path: Path) -> FakeStore: if path not in stores: - stores[path] = FakeStore(hass, path) + stores[path] = FakeStore(hass, path, ics_content) return stores[path] with patch( diff --git a/tests/components/local_calendar/test_calendar.py b/tests/components/local_calendar/test_calendar.py index a2f13ea289d..559a2af38b3 100644 --- a/tests/components/local_calendar/test_calendar.py +++ b/tests/components/local_calendar/test_calendar.py @@ -1,6 +1,7 @@ """Tests for calendar platform of local calendar.""" import datetime +import textwrap import pytest @@ -940,3 +941,91 @@ async def test_create_event_service( "location": "Test Location", } ] + + +@pytest.mark.parametrize( + "ics_content", + [ + textwrap.dedent( + """\ + BEGIN:VCALENDAR + BEGIN:VEVENT + SUMMARY:Bastille Day Party + DTSTART:19970714 + DTEND:19970714 + END:VEVENT + END:VCALENDAR + """ + ), + textwrap.dedent( + """\ + BEGIN:VCALENDAR + BEGIN:VEVENT + SUMMARY:Bastille Day Party + DTSTART:19970714 + DTEND:19970710 + END:VEVENT + END:VCALENDAR + """ + ), + ], + ids=["no_duration", "negative"], +) +async def test_invalid_all_day_event( + ws_client: ClientFixture, + setup_integration: None, + get_events: GetEventsFn, +) -> None: + """Test all day events with invalid durations, which are coerced to be valid.""" + events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"date": "1997-07-14"}, + "end": {"date": "1997-07-15"}, + } + ] + + +@pytest.mark.parametrize( + "ics_content", + [ + textwrap.dedent( + """\ + BEGIN:VCALENDAR + BEGIN:VEVENT + SUMMARY:Bastille Day Party + DTSTART:19970714T110000 + DTEND:19970714T110000 + END:VEVENT + END:VCALENDAR + """ + ), + textwrap.dedent( + """\ + BEGIN:VCALENDAR + BEGIN:VEVENT + SUMMARY:Bastille Day Party + DTSTART:19970714T110000 + DTEND:19970710T100000 + END:VEVENT + END:VCALENDAR + """ + ), + ], + ids=["no_duration", "negative"], +) +async def test_invalid_event_duration( + ws_client: ClientFixture, + setup_integration: None, + get_events: GetEventsFn, +) -> None: + """Test events with invalid durations, which are coerced to be valid.""" + events = await get_events("1997-07-14T00:00:00Z", "1997-07-16T00:00:00Z") + assert list(map(event_fields, events)) == [ + { + "summary": "Bastille Day Party", + "start": {"dateTime": "1997-07-14T11:00:00-06:00"}, + "end": {"dateTime": "1997-07-14T11:30:00-06:00"}, + } + ]