From 58aaf6fa97ae57bb189b9d5e929864b74a1e3852 Mon Sep 17 00:00:00 2001 From: Andre Faria Date: Mon, 21 Sep 2026 12:37:42 +0100 Subject: [PATCH] Fix Todoist timed calendar event duration (#182121) --- homeassistant/components/todoist/calendar.py | 9 ++- tests/components/todoist/test_calendar.py | 74 +++++++++++++++++--- 2 files changed, 72 insertions(+), 11 deletions(-) diff --git a/homeassistant/components/todoist/calendar.py b/homeassistant/components/todoist/calendar.py index 5fd9f38e5e92..b871327aee79 100644 --- a/homeassistant/components/todoist/calendar.py +++ b/homeassistant/components/todoist/calendar.py @@ -518,10 +518,15 @@ class TodoistProjectData: start = parse_due_date(task.due) if start is None: continue + duration = ( + timedelta(minutes=30) + if isinstance(start, datetime) + else timedelta(days=1) + ) event = CalendarEvent( summary=task.content, start=start, - end=start + timedelta(days=1), + end=start + duration, ) if ( event.start_datetime_local is not None @@ -530,7 +535,7 @@ class TodoistProjectData: continue if ( event.end_datetime_local is not None - and event.end_datetime_local < start_date + and event.end_datetime_local <= start_date ): continue events.append(event) diff --git a/tests/components/todoist/test_calendar.py b/tests/components/todoist/test_calendar.py index 1a1d3c20776d..26563e74fc56 100644 --- a/tests/components/todoist/test_calendar.py +++ b/tests/components/todoist/test_calendar.py @@ -221,6 +221,39 @@ async def test_calendar_custom_project_unique_id( assert entity is None +@pytest.mark.parametrize( + ("due", "expected_start", "expected_end"), + [ + ( + make_api_due(date="2023-03-30", string="Mar 30"), + {"date": "2023-03-30"}, + {"date": "2023-03-31"}, + ), + ( + make_api_due(date="2023-03-30T18:00:00", string="Mar 30 6:00 PM"), + {"dateTime": "2023-03-30T18:00:00-06:00"}, + {"dateTime": "2023-03-30T18:30:00-06:00"}, + ), + ], + ids=("all_day", "timed"), +) +async def test_event_duration( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + expected_start: dict[str, str], + expected_end: dict[str, str], +) -> None: + """Test date-only tasks last one day and timed tasks last 30 minutes.""" + client = await hass_client() + response = await client.get( + get_events_url( + "calendar.name", "2023-03-30T00:00:00.000Z", "2023-04-01T00:00:00.000Z" + ) + ) + assert response.status == HTTPStatus.OK + assert await response.json() == [get_events_response(expected_start, expected_end)] + + @pytest.mark.parametrize( ("due", "start", "end", "expected_response"), [ @@ -254,6 +287,12 @@ async def test_calendar_custom_project_unique_id( "2023-04-01T08:00:00.000Z", [], ), + ( + make_api_due(date="2023-03-30", is_recurring=False, string="Mar 30"), + "2023-03-31T06:00:00.000Z", + "2023-04-01T06:00:00.000Z", + [], + ), ( make_api_due(date="2023-03-30", is_recurring=False, string="Mar 30"), "2023-03-29T06:00:00.000Z", @@ -261,7 +300,15 @@ async def test_calendar_custom_project_unique_id( [], ), ], - ids=("included", "exact", "overlap_start", "overlap_end", "after", "before"), + ids=( + "included", + "exact", + "overlap_start", + "overlap_end", + "after", + "end_boundary", + "before", + ), ) async def test_all_day_event( hass: HomeAssistant, @@ -374,14 +421,14 @@ async def test_task_due_datetime( has_task_response = [ get_events_response( {"dateTime": "2023-03-30T18:00:00-06:00"}, - {"dateTime": "2023-03-31T18:00:00-06:00"}, + {"dateTime": "2023-03-30T18:30:00-06:00"}, ) ] # Completely includes the start/end of the task response = await client.get( get_events_url( - "calendar.name", "2023-03-30T08:00:00.000Z", "2023-03-31T08:00:00.000Z" + "calendar.name", "2023-03-30T23:00:00.000Z", "2023-03-31T01:00:00.000Z" ), ) assert response.status == HTTPStatus.OK @@ -390,7 +437,7 @@ async def test_task_due_datetime( # Overlap with the start of the event response = await client.get( get_events_url( - "calendar.name", "2023-03-29T20:00:00.000Z", "2023-03-31T02:00:00.000Z" + "calendar.name", "2023-03-30T23:30:00.000Z", "2023-03-31T00:15:00.000Z" ), ) assert response.status == HTTPStatus.OK @@ -399,7 +446,7 @@ async def test_task_due_datetime( # Overlap with the end of the event response = await client.get( get_events_url( - "calendar.name", "2023-03-31T20:00:00.000Z", "2023-04-01T02:00:00.000Z" + "calendar.name", "2023-03-31T00:15:00.000Z", "2023-03-31T01:00:00.000Z" ), ) assert response.status == HTTPStatus.OK @@ -408,7 +455,7 @@ async def test_task_due_datetime( # Task is active, but range does not include start/end response = await client.get( get_events_url( - "calendar.name", "2023-03-31T10:00:00.000Z", "2023-03-31T11:00:00.000Z" + "calendar.name", "2023-03-31T00:10:00.000Z", "2023-03-31T00:20:00.000Z" ), ) assert response.status == HTTPStatus.OK @@ -417,7 +464,16 @@ async def test_task_due_datetime( # Query is before the task starts (no results) response = await client.get( get_events_url( - "calendar.name", "2023-03-28T00:00:00.000Z", "2023-03-29T00:00:00.000Z" + "calendar.name", "2023-03-30T22:00:00.000Z", "2023-03-30T23:00:00.000Z" + ), + ) + assert response.status == HTTPStatus.OK + assert await response.json() == [] + + # Query starts exactly when the task ends (no results) + response = await client.get( + get_events_url( + "calendar.name", "2023-03-31T00:30:00.000Z", "2023-03-31T01:00:00.000Z" ), ) assert response.status == HTTPStatus.OK @@ -426,7 +482,7 @@ async def test_task_due_datetime( # Query is after the task ends (no results) response = await client.get( get_events_url( - "calendar.name", "2023-04-01T07:00:00.000Z", "2023-04-02T07:00:00.000Z" + "calendar.name", "2023-03-31T01:00:00.000Z", "2023-03-31T02:00:00.000Z" ), ) assert response.status == HTTPStatus.OK @@ -535,6 +591,6 @@ async def test_config_entry( assert await response.json() == [ get_events_response( {"dateTime": "2023-03-30T18:00:00-06:00"}, - {"dateTime": "2023-03-31T18:00:00-06:00"}, + {"dateTime": "2023-03-30T18:30:00-06:00"}, ) ]