2022-03-08 06:00:39 +00:00
|
|
|
"""Unit tests for the Todoist calendar platform."""
|
2024-03-08 13:44:56 +00:00
|
|
|
|
2023-05-27 17:09:11 +00:00
|
|
|
from datetime import timedelta
|
2023-03-26 20:08:36 +00:00
|
|
|
from http import HTTPStatus
|
2023-04-16 12:20:07 +00:00
|
|
|
from typing import Any
|
2022-12-30 17:49:35 +00:00
|
|
|
from unittest.mock import AsyncMock, patch
|
2023-03-26 20:08:36 +00:00
|
|
|
import urllib
|
2023-05-30 02:45:22 +00:00
|
|
|
import zoneinfo
|
2022-03-08 06:00:39 +00:00
|
|
|
|
2022-08-01 15:15:51 +00:00
|
|
|
import pytest
|
2023-09-12 05:56:08 +00:00
|
|
|
from todoist_api_python.models import Due
|
2022-08-01 15:15:51 +00:00
|
|
|
|
|
|
|
from homeassistant import setup
|
2023-03-28 07:33:32 +00:00
|
|
|
from homeassistant.components.todoist.const import (
|
|
|
|
ASSIGNEE,
|
|
|
|
CONTENT,
|
|
|
|
DOMAIN,
|
|
|
|
LABELS,
|
|
|
|
PROJECT_NAME,
|
|
|
|
SERVICE_NEW_TASK,
|
|
|
|
)
|
2023-10-24 20:47:26 +00:00
|
|
|
from homeassistant.const import CONF_TOKEN, Platform
|
2023-02-16 13:08:03 +00:00
|
|
|
from homeassistant.core import HomeAssistant
|
2023-03-01 15:23:36 +00:00
|
|
|
from homeassistant.helpers import entity_registry as er
|
2023-03-01 11:01:54 +00:00
|
|
|
from homeassistant.helpers.entity_component import async_update_entity
|
2023-05-29 21:00:43 +00:00
|
|
|
from homeassistant.util import dt as dt_util
|
2023-03-26 20:08:36 +00:00
|
|
|
|
2023-10-24 20:47:26 +00:00
|
|
|
from .conftest import PROJECT_ID, SUMMARY
|
2023-09-12 05:56:08 +00:00
|
|
|
|
2023-03-26 20:08:36 +00:00
|
|
|
from tests.typing import ClientSessionGenerator
|
2022-03-08 06:00:39 +00:00
|
|
|
|
2023-05-30 02:45:22 +00:00
|
|
|
# Set our timezone to CST/Regina so we can check calculations
|
|
|
|
# This keeps UTC-6 all year round
|
|
|
|
TZ_NAME = "America/Regina"
|
|
|
|
TIMEZONE = zoneinfo.ZoneInfo(TZ_NAME)
|
2023-04-16 12:20:07 +00:00
|
|
|
|
2022-03-08 06:00:39 +00:00
|
|
|
|
2023-10-24 20:47:26 +00:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def platforms() -> list[Platform]:
|
|
|
|
"""Override platforms."""
|
|
|
|
return [Platform.CALENDAR]
|
|
|
|
|
|
|
|
|
2023-04-14 04:12:58 +00:00
|
|
|
@pytest.fixture(autouse=True)
|
|
|
|
def set_time_zone(hass: HomeAssistant):
|
|
|
|
"""Set the time zone for the tests."""
|
2023-05-30 02:45:22 +00:00
|
|
|
hass.config.set_time_zone(TZ_NAME)
|
2023-04-14 04:12:58 +00:00
|
|
|
|
|
|
|
|
2023-03-26 20:08:36 +00:00
|
|
|
def get_events_url(entity: str, start: str, end: str) -> str:
|
|
|
|
"""Create a url to get events during the specified time range."""
|
|
|
|
return f"/api/calendars/{entity}?start={urllib.parse.quote(start)}&end={urllib.parse.quote(end)}"
|
|
|
|
|
|
|
|
|
2023-04-16 12:20:07 +00:00
|
|
|
def get_events_response(start: dict[str, str], end: dict[str, str]) -> dict[str, Any]:
|
|
|
|
"""Return an event response with a single task."""
|
|
|
|
return {
|
|
|
|
"start": start,
|
|
|
|
"end": end,
|
|
|
|
"summary": SUMMARY,
|
|
|
|
"description": None,
|
|
|
|
"location": None,
|
|
|
|
"uid": None,
|
|
|
|
"recurrence_id": None,
|
|
|
|
"rrule": None,
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
@pytest.fixture(name="todoist_config")
|
|
|
|
def mock_todoist_config() -> dict[str, Any]:
|
|
|
|
"""Mock todoist configuration."""
|
|
|
|
return {}
|
|
|
|
|
|
|
|
|
2023-09-12 05:56:08 +00:00
|
|
|
@pytest.fixture(name="setup_platform", autouse=True)
|
|
|
|
async def mock_setup_platform(
|
2023-04-16 12:20:07 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
api: AsyncMock,
|
|
|
|
todoist_config: dict[str, Any],
|
|
|
|
) -> None:
|
|
|
|
"""Mock setup of the todoist integration."""
|
|
|
|
with patch(
|
|
|
|
"homeassistant.components.todoist.calendar.TodoistAPIAsync"
|
|
|
|
) as todoist_api:
|
|
|
|
todoist_api.return_value = api
|
|
|
|
assert await setup.async_setup_component(
|
|
|
|
hass,
|
|
|
|
"calendar",
|
|
|
|
{
|
|
|
|
"calendar": {
|
|
|
|
"platform": DOMAIN,
|
|
|
|
CONF_TOKEN: "token",
|
|
|
|
**todoist_config,
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
await async_update_entity(hass, "calendar.name")
|
|
|
|
yield
|
|
|
|
|
|
|
|
|
2023-03-01 15:23:36 +00:00
|
|
|
async def test_calendar_entity_unique_id(
|
2023-04-16 12:20:07 +00:00
|
|
|
hass: HomeAssistant, api: AsyncMock, entity_registry: er.EntityRegistry
|
2023-03-01 15:23:36 +00:00
|
|
|
) -> None:
|
2022-08-01 15:15:51 +00:00
|
|
|
"""Test unique id is set to project id."""
|
2023-03-01 15:23:36 +00:00
|
|
|
entity = entity_registry.async_get("calendar.name")
|
2023-10-24 20:47:26 +00:00
|
|
|
assert entity.unique_id == PROJECT_ID
|
2022-08-01 15:15:51 +00:00
|
|
|
|
|
|
|
|
2023-04-16 12:20:07 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"todoist_config",
|
|
|
|
[{"custom_projects": [{"name": "All projects", "labels": ["Label1"]}]}],
|
|
|
|
)
|
2023-03-10 11:06:50 +00:00
|
|
|
async def test_update_entity_for_custom_project_with_labels_on(
|
2023-04-16 12:20:07 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
api: AsyncMock,
|
2023-03-10 11:06:50 +00:00
|
|
|
) -> None:
|
2023-03-01 11:01:54 +00:00
|
|
|
"""Test that the calendar's state is on for a custom project using labels."""
|
|
|
|
await async_update_entity(hass, "calendar.all_projects")
|
|
|
|
state = hass.states.get("calendar.all_projects")
|
|
|
|
assert state.attributes["labels"] == ["Label1"]
|
|
|
|
assert state.state == "on"
|
|
|
|
|
|
|
|
|
2023-04-16 12:20:07 +00:00
|
|
|
@pytest.mark.parametrize("due", [None])
|
2023-04-14 04:12:58 +00:00
|
|
|
async def test_update_entity_for_custom_project_no_due_date_on(
|
2023-04-16 12:20:07 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
api: AsyncMock,
|
2023-04-14 04:12:58 +00:00
|
|
|
) -> None:
|
|
|
|
"""Test that a task without an explicit due date is considered to be in an on state."""
|
2023-04-16 12:20:07 +00:00
|
|
|
await async_update_entity(hass, "calendar.name")
|
|
|
|
state = hass.states.get("calendar.name")
|
2023-04-14 04:12:58 +00:00
|
|
|
assert state.state == "on"
|
|
|
|
|
|
|
|
|
2023-05-27 17:09:11 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"due",
|
|
|
|
[
|
|
|
|
Due(
|
2023-05-30 02:45:22 +00:00
|
|
|
# Note: This runs before the test fixture that sets the timezone
|
|
|
|
date=(dt_util.now(TIMEZONE) + timedelta(days=3)).strftime("%Y-%m-%d"),
|
2023-05-27 17:09:11 +00:00
|
|
|
is_recurring=False,
|
|
|
|
string="3 days from today",
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
async def test_update_entity_for_calendar_with_due_date_in_the_future(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
api: AsyncMock,
|
|
|
|
) -> None:
|
|
|
|
"""Test that a task with a due date in the future has on state and correct end_time."""
|
|
|
|
await async_update_entity(hass, "calendar.name")
|
|
|
|
state = hass.states.get("calendar.name")
|
|
|
|
assert state.state == "on"
|
|
|
|
|
|
|
|
# The end time should be in the user's timezone
|
2023-05-29 21:00:43 +00:00
|
|
|
expected_end_time = (dt_util.now() + timedelta(days=3)).strftime(
|
|
|
|
"%Y-%m-%d 00:00:00"
|
|
|
|
)
|
2023-05-27 17:09:11 +00:00
|
|
|
assert state.attributes["end_time"] == expected_end_time
|
|
|
|
|
|
|
|
|
2023-09-12 05:56:08 +00:00
|
|
|
@pytest.mark.parametrize("setup_platform", [None])
|
2023-04-16 12:20:07 +00:00
|
|
|
async def test_failed_coordinator_update(hass: HomeAssistant, api: AsyncMock) -> None:
|
2023-03-28 16:57:24 +00:00
|
|
|
"""Test a failed data coordinator update is handled correctly."""
|
|
|
|
api.get_tasks.side_effect = Exception("API error")
|
|
|
|
|
|
|
|
assert await setup.async_setup_component(
|
|
|
|
hass,
|
|
|
|
"calendar",
|
|
|
|
{
|
|
|
|
"calendar": {
|
|
|
|
"platform": DOMAIN,
|
|
|
|
CONF_TOKEN: "token",
|
|
|
|
"custom_projects": [{"name": "All projects", "labels": ["Label1"]}],
|
|
|
|
}
|
|
|
|
},
|
|
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
await async_update_entity(hass, "calendar.all_projects")
|
|
|
|
state = hass.states.get("calendar.all_projects")
|
|
|
|
assert state is None
|
|
|
|
|
|
|
|
|
2023-04-16 12:20:07 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
"todoist_config",
|
|
|
|
[{"custom_projects": [{"name": "All projects"}]}],
|
|
|
|
)
|
2023-02-16 13:08:03 +00:00
|
|
|
async def test_calendar_custom_project_unique_id(
|
2023-04-16 12:20:07 +00:00
|
|
|
hass: HomeAssistant, entity_registry: er.EntityRegistry
|
2023-02-16 13:08:03 +00:00
|
|
|
) -> None:
|
2022-08-01 15:15:51 +00:00
|
|
|
"""Test unique id is None for any custom projects."""
|
2023-03-01 15:23:36 +00:00
|
|
|
entity = entity_registry.async_get("calendar.all_projects")
|
2022-08-01 15:15:51 +00:00
|
|
|
assert entity is None
|
|
|
|
|
2023-03-26 20:08:36 +00:00
|
|
|
|
2023-04-16 12:20:07 +00:00
|
|
|
@pytest.mark.parametrize(
|
|
|
|
("due", "start", "end", "expected_response"),
|
|
|
|
[
|
|
|
|
(
|
|
|
|
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
|
|
|
"2023-03-28T00:00:00.000Z",
|
|
|
|
"2023-04-01T00:00:00.000Z",
|
|
|
|
[get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})],
|
2023-04-17 12:21:49 +00:00
|
|
|
),
|
|
|
|
(
|
|
|
|
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
|
|
|
"2023-03-30T06:00:00.000Z",
|
|
|
|
"2023-03-31T06:00:00.000Z",
|
|
|
|
[get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})],
|
|
|
|
),
|
|
|
|
(
|
|
|
|
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
|
|
|
"2023-03-29T08:00:00.000Z",
|
|
|
|
"2023-03-30T08:00:00.000Z",
|
|
|
|
[get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})],
|
|
|
|
),
|
|
|
|
(
|
|
|
|
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
|
|
|
"2023-03-30T08:00:00.000Z",
|
|
|
|
"2023-03-31T08:00:00.000Z",
|
|
|
|
[get_events_response({"date": "2023-03-30"}, {"date": "2023-03-31"})],
|
|
|
|
),
|
|
|
|
(
|
|
|
|
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
|
|
|
"2023-03-31T08:00:00.000Z",
|
|
|
|
"2023-04-01T08:00:00.000Z",
|
|
|
|
[],
|
|
|
|
),
|
|
|
|
(
|
|
|
|
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
|
|
|
"2023-03-29T06:00:00.000Z",
|
|
|
|
"2023-03-30T06:00:00.000Z",
|
|
|
|
[],
|
|
|
|
),
|
2023-04-16 12:20:07 +00:00
|
|
|
],
|
2023-04-17 12:21:49 +00:00
|
|
|
ids=("included", "exact", "overlap_start", "overlap_end", "after", "before"),
|
2023-04-16 12:20:07 +00:00
|
|
|
)
|
2023-03-26 20:08:36 +00:00
|
|
|
async def test_all_day_event(
|
2023-04-16 12:20:07 +00:00
|
|
|
hass: HomeAssistant,
|
|
|
|
hass_client: ClientSessionGenerator,
|
|
|
|
start: str,
|
|
|
|
end: str,
|
|
|
|
expected_response: dict[str, Any],
|
2023-03-26 20:08:36 +00:00
|
|
|
) -> None:
|
|
|
|
"""Test for an all day calendar event."""
|
|
|
|
client = await hass_client()
|
|
|
|
response = await client.get(
|
2023-04-16 12:20:07 +00:00
|
|
|
get_events_url("calendar.name", start, end),
|
2023-03-26 20:08:36 +00:00
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
2023-04-16 12:20:07 +00:00
|
|
|
assert await response.json() == expected_response
|
2023-03-28 07:33:32 +00:00
|
|
|
|
|
|
|
|
2023-04-16 12:20:07 +00:00
|
|
|
async def test_create_task_service_call(hass: HomeAssistant, api: AsyncMock) -> None:
|
2023-03-28 07:33:32 +00:00
|
|
|
"""Test api is called correctly after a new task service call."""
|
|
|
|
await hass.services.async_call(
|
|
|
|
DOMAIN,
|
|
|
|
SERVICE_NEW_TASK,
|
|
|
|
{ASSIGNEE: "user", CONTENT: "task", LABELS: ["Label1"], PROJECT_NAME: "Name"},
|
|
|
|
)
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
|
|
|
|
api.add_task.assert_called_with(
|
2023-10-24 20:47:26 +00:00
|
|
|
"task", project_id=PROJECT_ID, labels=["Label1"], assignee_id="1"
|
2023-03-28 07:33:32 +00:00
|
|
|
)
|
2023-04-17 12:21:49 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
("due"),
|
|
|
|
[
|
|
|
|
# These are all equivalent due dates for the same time in different
|
|
|
|
# timezone formats.
|
|
|
|
Due(
|
|
|
|
date="2023-03-30",
|
|
|
|
is_recurring=False,
|
|
|
|
string="Mar 30 6:00 PM",
|
|
|
|
datetime="2023-03-31T00:00:00Z",
|
|
|
|
timezone="America/Regina",
|
|
|
|
),
|
|
|
|
Due(
|
|
|
|
date="2023-03-30",
|
|
|
|
is_recurring=False,
|
|
|
|
string="Mar 30 7:00 PM",
|
|
|
|
datetime="2023-03-31T00:00:00Z",
|
|
|
|
timezone="America/Los_Angeles",
|
|
|
|
),
|
|
|
|
Due(
|
|
|
|
date="2023-03-30",
|
|
|
|
is_recurring=False,
|
|
|
|
string="Mar 30 6:00 PM",
|
|
|
|
datetime="2023-03-30T18:00:00",
|
|
|
|
),
|
|
|
|
],
|
|
|
|
ids=("in_local_timezone", "in_other_timezone", "floating"),
|
|
|
|
)
|
|
|
|
async def test_task_due_datetime(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
hass_client: ClientSessionGenerator,
|
|
|
|
) -> None:
|
|
|
|
"""Test for task due at a specific time, using different time formats."""
|
|
|
|
client = await hass_client()
|
|
|
|
|
|
|
|
has_task_response = [
|
|
|
|
get_events_response(
|
|
|
|
{"dateTime": "2023-03-30T18:00:00-06:00"},
|
|
|
|
{"dateTime": "2023-03-31T18:00: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"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == has_task_response
|
|
|
|
|
|
|
|
# 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"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == has_task_response
|
|
|
|
|
|
|
|
# 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"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == has_task_response
|
|
|
|
|
|
|
|
# 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"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == has_task_response
|
|
|
|
|
|
|
|
# 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"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == []
|
|
|
|
|
|
|
|
# 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"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == []
|
2023-09-12 05:56:08 +00:00
|
|
|
|
|
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
|
|
("due", "setup_platform"),
|
|
|
|
[
|
|
|
|
(
|
|
|
|
Due(
|
|
|
|
date="2023-03-30",
|
|
|
|
is_recurring=False,
|
|
|
|
string="Mar 30 6:00 PM",
|
|
|
|
datetime="2023-03-31T00:00:00Z",
|
|
|
|
timezone="America/Regina",
|
|
|
|
),
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
],
|
|
|
|
)
|
|
|
|
async def test_config_entry(
|
|
|
|
hass: HomeAssistant,
|
|
|
|
setup_integration: None,
|
|
|
|
hass_client: ClientSessionGenerator,
|
|
|
|
) -> None:
|
|
|
|
"""Test for a calendar created with a config entry."""
|
|
|
|
|
|
|
|
await async_update_entity(hass, "calendar.name")
|
|
|
|
state = hass.states.get("calendar.name")
|
|
|
|
assert state
|
|
|
|
|
|
|
|
client = await hass_client()
|
|
|
|
response = await client.get(
|
|
|
|
get_events_url(
|
|
|
|
"calendar.name", "2023-03-30T08:00:00.000Z", "2023-03-31T08:00:00.000Z"
|
|
|
|
),
|
|
|
|
)
|
|
|
|
assert response.status == HTTPStatus.OK
|
|
|
|
assert await response.json() == [
|
|
|
|
get_events_response(
|
|
|
|
{"dateTime": "2023-03-30T18:00:00-06:00"},
|
|
|
|
{"dateTime": "2023-03-31T18:00:00-06:00"},
|
|
|
|
)
|
|
|
|
]
|