mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Update todoist-api-python to 3.1.0 (#161811)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -8,9 +8,7 @@ from typing import Any
|
||||
import uuid
|
||||
|
||||
from todoist_api_python.api_async import TodoistAPIAsync
|
||||
from todoist_api_python.endpoints import get_sync_url
|
||||
from todoist_api_python.headers import create_headers
|
||||
from todoist_api_python.models import Due, Label, Task
|
||||
from todoist_api_python.models import Label, Project, Task
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
@@ -62,8 +60,9 @@ from .const import (
|
||||
START,
|
||||
SUMMARY,
|
||||
)
|
||||
from .coordinator import TodoistCoordinator
|
||||
from .coordinator import TodoistCoordinator, flatten_async_pages
|
||||
from .types import CalData, CustomProject, ProjectData, TodoistEvent
|
||||
from .util import parse_due_date
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -157,18 +156,22 @@ async def async_setup_platform(
|
||||
|
||||
# Setup devices:
|
||||
# Grab all projects.
|
||||
projects = await api.get_projects()
|
||||
projects_result = await api.get_projects()
|
||||
all_projects: list[Project] = await flatten_async_pages(projects_result)
|
||||
|
||||
# Grab all labels
|
||||
labels = await api.get_labels()
|
||||
labels_result = await api.get_labels()
|
||||
all_labels: list[Label] = await flatten_async_pages(labels_result)
|
||||
|
||||
# Add all Todoist-defined projects.
|
||||
project_devices = []
|
||||
for project in projects:
|
||||
for project in all_projects:
|
||||
# Project is an object, not a dict!
|
||||
# Because of that, we convert what we need to a dict.
|
||||
project_data: ProjectData = {CONF_NAME: project.name, CONF_ID: project.id}
|
||||
project_devices.append(TodoistProjectEntity(coordinator, project_data, labels))
|
||||
project_devices.append(
|
||||
TodoistProjectEntity(coordinator, project_data, all_labels)
|
||||
)
|
||||
# Cache the names so we can easily look up name->ID.
|
||||
project_id_lookup[project.name.lower()] = project.id
|
||||
|
||||
@@ -196,7 +199,7 @@ async def async_setup_platform(
|
||||
TodoistProjectEntity(
|
||||
coordinator,
|
||||
{"id": None, "name": extra_project["name"]},
|
||||
labels,
|
||||
all_labels,
|
||||
due_date_days=project_due_date,
|
||||
whitelisted_labels=project_label_filter,
|
||||
whitelisted_projects=project_id_filter,
|
||||
@@ -218,7 +221,7 @@ def async_register_services( # noqa: C901
|
||||
|
||||
session = async_get_clientsession(hass)
|
||||
|
||||
async def handle_new_task(call: ServiceCall) -> None: # noqa: C901
|
||||
async def handle_new_task(call: ServiceCall) -> None:
|
||||
"""Call when a user creates a new Todoist Task from Home Assistant."""
|
||||
project_name = call.data[PROJECT_NAME]
|
||||
projects = await coordinator.async_get_projects()
|
||||
@@ -269,9 +272,10 @@ def async_register_services( # noqa: C901
|
||||
data["labels"] = task_labels
|
||||
|
||||
if ASSIGNEE in call.data:
|
||||
collaborators = await coordinator.api.get_collaborators(project_id)
|
||||
collaborators_result = await coordinator.api.get_collaborators(project_id)
|
||||
all_collaborators = await flatten_async_pages(collaborators_result)
|
||||
collaborator_id_lookup = {
|
||||
collab.name.lower(): collab.id for collab in collaborators
|
||||
collab.name.lower(): collab.id for collab in all_collaborators
|
||||
}
|
||||
task_assignee = call.data[ASSIGNEE].lower()
|
||||
if task_assignee in collaborator_id_lookup:
|
||||
@@ -297,17 +301,14 @@ def async_register_services( # noqa: C901
|
||||
if due is None:
|
||||
raise ValueError(f"Invalid due_date: {call.data[DUE_DATE]}")
|
||||
due_date = datetime(due.year, due.month, due.day)
|
||||
# Format it in the manner Todoist expects
|
||||
due_date = dt_util.as_utc(due_date)
|
||||
date_format = "%Y-%m-%dT%H:%M:%S"
|
||||
data["due_datetime"] = datetime.strftime(due_date, date_format)
|
||||
# Pass the datetime object directly - the library handles formatting
|
||||
data["due_datetime"] = dt_util.as_utc(due_date)
|
||||
|
||||
api_task = await coordinator.api.add_task(content, **data)
|
||||
|
||||
# @NOTE: The rest-api doesn't support reminders, this works manually using
|
||||
# the sync api, in order to keep functional parity with the component.
|
||||
# https://developer.todoist.com/sync/v9/#reminders
|
||||
sync_url = get_sync_url("sync")
|
||||
# The REST API doesn't support reminders, so we use the Sync API directly
|
||||
# to maintain functional parity with the component.
|
||||
# https://developer.todoist.com/api/v1/#tag/Sync/Reminders/Add-a-reminder
|
||||
_reminder_due: dict = {}
|
||||
if REMINDER_DATE_STRING in call.data:
|
||||
_reminder_due["string"] = call.data[REMINDER_DATE_STRING]
|
||||
@@ -316,20 +317,21 @@ def async_register_services( # noqa: C901
|
||||
_reminder_due["lang"] = call.data[REMINDER_DATE_LANG]
|
||||
|
||||
if REMINDER_DATE in call.data:
|
||||
due_date = dt_util.parse_datetime(call.data[REMINDER_DATE])
|
||||
if due_date is None:
|
||||
due = dt_util.parse_date(call.data[REMINDER_DATE])
|
||||
if due is None:
|
||||
reminder_date = dt_util.parse_datetime(call.data[REMINDER_DATE])
|
||||
if reminder_date is None:
|
||||
reminder = dt_util.parse_date(call.data[REMINDER_DATE])
|
||||
if reminder is None:
|
||||
raise ValueError(
|
||||
f"Invalid reminder_date: {call.data[REMINDER_DATE]}"
|
||||
)
|
||||
due_date = datetime(due.year, due.month, due.day)
|
||||
# Format it in the manner Todoist expects
|
||||
due_date = dt_util.as_utc(due_date)
|
||||
date_format = "%Y-%m-%dT%H:%M:%S"
|
||||
_reminder_due["date"] = datetime.strftime(due_date, date_format)
|
||||
reminder_date = datetime(reminder.year, reminder.month, reminder.day)
|
||||
# Format it in the manner Todoist expects (UTC with Z suffix)
|
||||
reminder_date = dt_util.as_utc(reminder_date)
|
||||
date_format = "%Y-%m-%dT%H:%M:%S.000000Z"
|
||||
_reminder_due["date"] = datetime.strftime(reminder_date, date_format)
|
||||
|
||||
async def add_reminder(reminder_due: dict):
|
||||
if _reminder_due:
|
||||
sync_url = "https://api.todoist.com/api/v1/sync"
|
||||
reminder_data = {
|
||||
"commands": [
|
||||
{
|
||||
@@ -339,16 +341,16 @@ def async_register_services( # noqa: C901
|
||||
"args": {
|
||||
"item_id": api_task.id,
|
||||
"type": "absolute",
|
||||
"due": reminder_due,
|
||||
"due": _reminder_due,
|
||||
},
|
||||
}
|
||||
]
|
||||
}
|
||||
headers = create_headers(token=coordinator.token, with_content=True)
|
||||
return await session.post(sync_url, headers=headers, json=reminder_data)
|
||||
|
||||
if _reminder_due:
|
||||
await add_reminder(_reminder_due)
|
||||
headers = {
|
||||
"Authorization": f"Bearer {coordinator.token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
await session.post(sync_url, headers=headers, json=reminder_data)
|
||||
|
||||
_LOGGER.debug("Created Todoist task: %s", call.data[CONTENT])
|
||||
|
||||
@@ -527,7 +529,7 @@ class TodoistProjectData:
|
||||
"""
|
||||
task: TodoistEvent = {
|
||||
ALL_DAY: False,
|
||||
COMPLETED: data.is_completed,
|
||||
COMPLETED: data.completed_at is not None,
|
||||
DESCRIPTION: f"https://todoist.com/showTask?id={data.id}",
|
||||
DUE_TODAY: False,
|
||||
END: None,
|
||||
@@ -561,22 +563,26 @@ class TodoistProjectData:
|
||||
# complete the task.
|
||||
# Generally speaking, that means right now.
|
||||
if data.due is not None:
|
||||
end = dt_util.parse_datetime(
|
||||
data.due.datetime if data.due.datetime else data.due.date
|
||||
)
|
||||
task[END] = dt_util.as_local(end) if end is not None else end
|
||||
if task[END] is not None:
|
||||
if self._due_date_days is not None and (
|
||||
task[END] > dt_util.now() + self._due_date_days
|
||||
):
|
||||
# This task is out of range of our due date;
|
||||
# it shouldn't be counted.
|
||||
return None
|
||||
due_date = data.due.date
|
||||
# The API returns date or datetime objects when deserialized via from_dict()
|
||||
if isinstance(due_date, datetime):
|
||||
task[END] = dt_util.as_local(due_date)
|
||||
elif isinstance(due_date, date):
|
||||
task[END] = dt_util.start_of_local_day(due_date)
|
||||
|
||||
task[DUE_TODAY] = task[END].date() == dt_util.now().date()
|
||||
if (end_dt := task[END]) is not None:
|
||||
if self._due_date_days is not None:
|
||||
# For comparison with now, use datetime
|
||||
|
||||
if end_dt > dt_util.now() + self._due_date_days:
|
||||
# This task is out of range of our due date;
|
||||
# it shouldn't be counted.
|
||||
return None
|
||||
|
||||
task[DUE_TODAY] = end_dt.date() == dt_util.now().date()
|
||||
|
||||
# Special case: Task is overdue.
|
||||
if task[END] <= task[START]:
|
||||
if end_dt <= task[START]:
|
||||
task[OVERDUE] = True
|
||||
# Set end time to the current time plus 1 hour.
|
||||
# We're pretty much guaranteed to update within that 1 hour,
|
||||
@@ -681,7 +687,7 @@ class TodoistProjectData:
|
||||
for task in project_task_data:
|
||||
if task.due is None:
|
||||
continue
|
||||
start = get_start(task.due)
|
||||
start = parse_due_date(task.due)
|
||||
if start is None:
|
||||
continue
|
||||
event = CalendarEvent(
|
||||
@@ -689,9 +695,15 @@ class TodoistProjectData:
|
||||
start=start,
|
||||
end=start + timedelta(days=1),
|
||||
)
|
||||
if event.start_datetime_local >= end_date:
|
||||
if (
|
||||
event.start_datetime_local is not None
|
||||
and event.start_datetime_local >= end_date
|
||||
):
|
||||
continue
|
||||
if event.end_datetime_local < start_date:
|
||||
if (
|
||||
event.end_datetime_local is not None
|
||||
and event.end_datetime_local < start_date
|
||||
):
|
||||
continue
|
||||
events.append(event)
|
||||
return events
|
||||
@@ -748,15 +760,3 @@ class TodoistProjectData:
|
||||
return
|
||||
self.event = event
|
||||
_LOGGER.debug("Updated %s", self._name)
|
||||
|
||||
|
||||
def get_start(due: Due) -> datetime | date | None:
|
||||
"""Return the task due date as a start date or date time."""
|
||||
if due.datetime:
|
||||
start = dt_util.parse_datetime(due.datetime)
|
||||
if not start:
|
||||
return None
|
||||
return dt_util.as_local(start)
|
||||
if due.date:
|
||||
return dt_util.parse_date(due.date)
|
||||
return None
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
"""DataUpdateCoordinator for the Todoist component."""
|
||||
|
||||
from collections.abc import AsyncGenerator
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import TypeVar
|
||||
|
||||
from todoist_api_python.api_async import TodoistAPIAsync
|
||||
from todoist_api_python.models import Label, Project, Section, Task
|
||||
@@ -10,6 +12,18 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
|
||||
async def flatten_async_pages(
|
||||
pages: AsyncGenerator[list[T]],
|
||||
) -> list[T]:
|
||||
"""Flatten paginated results from an async generator."""
|
||||
all_items: list[T] = []
|
||||
async for page in pages:
|
||||
all_items.extend(page)
|
||||
return all_items
|
||||
|
||||
|
||||
class TodoistCoordinator(DataUpdateCoordinator[list[Task]]):
|
||||
"""Coordinator for updating task data from Todoist."""
|
||||
@@ -39,22 +53,26 @@ class TodoistCoordinator(DataUpdateCoordinator[list[Task]]):
|
||||
async def _async_update_data(self) -> list[Task]:
|
||||
"""Fetch tasks from the Todoist API."""
|
||||
try:
|
||||
return await self.api.get_tasks()
|
||||
tasks_async = await self.api.get_tasks()
|
||||
except Exception as err:
|
||||
raise UpdateFailed(f"Error communicating with API: {err}") from err
|
||||
return await flatten_async_pages(tasks_async)
|
||||
|
||||
async def async_get_projects(self) -> list[Project]:
|
||||
"""Return todoist projects fetched at most once."""
|
||||
if self._projects is None:
|
||||
self._projects = await self.api.get_projects()
|
||||
projects_async = await self.api.get_projects()
|
||||
self._projects = await flatten_async_pages(projects_async)
|
||||
return self._projects
|
||||
|
||||
async def async_get_sections(self, project_id: str) -> list[Section]:
|
||||
"""Return todoist sections for a given project ID."""
|
||||
return await self.api.get_sections(project_id=project_id)
|
||||
sections_async = await self.api.get_sections(project_id=project_id)
|
||||
return await flatten_async_pages(sections_async)
|
||||
|
||||
async def async_get_labels(self) -> list[Label]:
|
||||
"""Return todoist labels fetched at most once."""
|
||||
if self._labels is None:
|
||||
self._labels = await self.api.get_labels()
|
||||
labels_async = await self.api.get_labels()
|
||||
self._labels = await flatten_async_pages(labels_async)
|
||||
return self._labels
|
||||
|
||||
@@ -6,5 +6,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/todoist",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["todoist"],
|
||||
"requirements": ["todoist-api-python==2.1.7"]
|
||||
"requirements": ["todoist-api-python==3.1.0"]
|
||||
}
|
||||
|
||||
@@ -16,10 +16,10 @@ from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import TodoistCoordinator
|
||||
from .util import parse_due_date
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
@@ -99,24 +99,16 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||
if task.parent_id is not None:
|
||||
# Filter out sub-tasks until they are supported by the UI.
|
||||
continue
|
||||
if task.is_completed:
|
||||
if task.completed_at is not None:
|
||||
status = TodoItemStatus.COMPLETED
|
||||
else:
|
||||
status = TodoItemStatus.NEEDS_ACTION
|
||||
due: datetime.date | datetime.datetime | None = None
|
||||
if task_due := task.due:
|
||||
if task_due.datetime:
|
||||
due = dt_util.as_local(
|
||||
datetime.datetime.fromisoformat(task_due.datetime)
|
||||
)
|
||||
elif task_due.date:
|
||||
due = datetime.date.fromisoformat(task_due.date)
|
||||
items.append(
|
||||
TodoItem(
|
||||
summary=task.content,
|
||||
uid=task.id,
|
||||
status=status,
|
||||
due=due,
|
||||
due=parse_due_date(task.due),
|
||||
description=task.description or None, # Don't use empty string
|
||||
)
|
||||
)
|
||||
@@ -147,9 +139,9 @@ class TodoistTodoListEntity(CoordinatorEntity[TodoistCoordinator], TodoListEntit
|
||||
|
||||
if item.status != existing_item.status:
|
||||
if item.status == TodoItemStatus.COMPLETED:
|
||||
await self.coordinator.api.close_task(task_id=uid)
|
||||
await self.coordinator.api.complete_task(task_id=uid)
|
||||
else:
|
||||
await self.coordinator.api.reopen_task(task_id=uid)
|
||||
await self.coordinator.api.uncomplete_task(task_id=uid)
|
||||
await self.coordinator.async_refresh()
|
||||
|
||||
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||
|
||||
35
homeassistant/components/todoist/util.py
Normal file
35
homeassistant/components/todoist/util.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""Utility functions for the Todoist integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import date, datetime
|
||||
|
||||
from todoist_api_python.models import Due
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
def parse_due_date(task_due: Due | None) -> date | datetime | None:
|
||||
"""Parse due date from Todoist task due object.
|
||||
|
||||
The due.date field contains either a date object (for date-only tasks)
|
||||
or a datetime object (for tasks with a specific time). When deserialized
|
||||
from the API via from_dict(), these are already proper Python date/datetime
|
||||
objects.
|
||||
|
||||
Args:
|
||||
task_due: The Due object from a Todoist task, or None.
|
||||
|
||||
Returns:
|
||||
A date object for date-only due dates, a localized datetime for
|
||||
datetime due dates, or None if no due date is set.
|
||||
|
||||
"""
|
||||
if task_due is None or not (due_date := task_due.date):
|
||||
return None
|
||||
|
||||
if isinstance(due_date, datetime):
|
||||
return dt_util.as_local(due_date)
|
||||
if isinstance(due_date, date):
|
||||
return due_date
|
||||
return None
|
||||
2
requirements_all.txt
generated
2
requirements_all.txt
generated
@@ -3040,7 +3040,7 @@ tilt-pi==0.2.1
|
||||
tmb==0.0.4
|
||||
|
||||
# homeassistant.components.todoist
|
||||
todoist-api-python==2.1.7
|
||||
todoist-api-python==3.1.0
|
||||
|
||||
# homeassistant.components.togrill
|
||||
togrill-bluetooth==0.8.1
|
||||
|
||||
2
requirements_test_all.txt
generated
2
requirements_test_all.txt
generated
@@ -2540,7 +2540,7 @@ tilt-ble==1.0.1
|
||||
tilt-pi==0.2.1
|
||||
|
||||
# homeassistant.components.todoist
|
||||
todoist-api-python==2.1.7
|
||||
todoist-api-python==3.1.0
|
||||
|
||||
# homeassistant.components.togrill
|
||||
togrill-bluetooth==0.8.1
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
"""Common fixtures for the todoist tests."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from collections.abc import AsyncGenerator, Callable, Generator
|
||||
from http import HTTPStatus
|
||||
from typing import TypeVar
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
import pytest
|
||||
@@ -13,15 +14,55 @@ from homeassistant.components.todoist import DOMAIN
|
||||
from homeassistant.const import CONF_TOKEN, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
T = TypeVar("T")
|
||||
|
||||
PROJECT_ID = "project-id-1"
|
||||
SECTION_ID = "section-id-1"
|
||||
SUMMARY = "A task"
|
||||
TOKEN = "some-token"
|
||||
TODAY = dt_util.now().strftime("%Y-%m-%d")
|
||||
|
||||
|
||||
async def _async_generator(items: list[T]) -> AsyncGenerator[list[T]]:
|
||||
"""Create an async generator that yields items as a single page."""
|
||||
yield items
|
||||
|
||||
|
||||
def make_api_response(items: list[T]) -> Callable[[], AsyncGenerator[list[T]]]:
|
||||
"""Create a callable that returns a fresh async generator each time.
|
||||
|
||||
This is needed because async generators can only be iterated once,
|
||||
but mocks may be called multiple times.
|
||||
"""
|
||||
|
||||
async def _generator(*args, **kwargs) -> AsyncGenerator[list[T]]:
|
||||
async for page in _async_generator(items):
|
||||
yield page
|
||||
|
||||
return _generator
|
||||
|
||||
|
||||
def make_api_due(
|
||||
date: str,
|
||||
is_recurring: bool = False,
|
||||
string: str = "",
|
||||
timezone: str | None = None,
|
||||
) -> Due:
|
||||
"""Create a Due object using from_dict to match API deserialization behavior.
|
||||
|
||||
This ensures the date field is properly converted to date/datetime objects
|
||||
just like the real API response deserialization does.
|
||||
"""
|
||||
data: dict = {
|
||||
"date": date,
|
||||
"is_recurring": is_recurring,
|
||||
"string": string,
|
||||
}
|
||||
if timezone is not None:
|
||||
data["timezone"] = timezone
|
||||
return Due.from_dict(data)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
@@ -35,16 +76,18 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
|
||||
@pytest.fixture(name="due")
|
||||
def mock_due() -> Due:
|
||||
"""Mock a todoist Task Due date/time."""
|
||||
return Due(
|
||||
is_recurring=False, date=dt_util.now().strftime("%Y-%m-%d"), string="today"
|
||||
)
|
||||
"""Mock a todoist Task Due date/time.
|
||||
|
||||
Uses a fixed date matching the frozen test time in test_calendar.py
|
||||
and test_todo.py (2024-05-24 12:00:00).
|
||||
"""
|
||||
return make_api_due(date="2024-05-24", string="today")
|
||||
|
||||
|
||||
def make_api_task(
|
||||
id: str | None = None,
|
||||
content: str | None = None,
|
||||
is_completed: bool = False,
|
||||
completed_at: str | None = None,
|
||||
due: Due | None = None,
|
||||
project_id: str | None = None,
|
||||
description: str | None = None,
|
||||
@@ -54,12 +97,11 @@ def make_api_task(
|
||||
return Task(
|
||||
assignee_id="1",
|
||||
assigner_id="1",
|
||||
comment_count=0,
|
||||
is_completed=is_completed,
|
||||
completed_at=completed_at,
|
||||
content=content or SUMMARY,
|
||||
created_at="2021-10-01T00:00:00",
|
||||
creator_id="1",
|
||||
description=description,
|
||||
description=description or "",
|
||||
due=due,
|
||||
id=id or "1",
|
||||
labels=["Label1"],
|
||||
@@ -68,9 +110,10 @@ def make_api_task(
|
||||
priority=1,
|
||||
project_id=project_id or PROJECT_ID,
|
||||
section_id=None,
|
||||
url="https://todoist.com",
|
||||
sync_id=None,
|
||||
duration=None,
|
||||
deadline=None,
|
||||
is_collapsed=False,
|
||||
updated_at="2021-10-01T00:00:00",
|
||||
)
|
||||
|
||||
|
||||
@@ -84,38 +127,45 @@ def mock_tasks(due: Due) -> list[Task]:
|
||||
def mock_api(tasks: list[Task]) -> AsyncMock:
|
||||
"""Mock the api state."""
|
||||
api = AsyncMock()
|
||||
api.get_projects.return_value = [
|
||||
Project(
|
||||
id=PROJECT_ID,
|
||||
color="blue",
|
||||
comment_count=0,
|
||||
is_favorite=False,
|
||||
name="Name",
|
||||
is_shared=False,
|
||||
url="",
|
||||
is_inbox_project=False,
|
||||
is_team_inbox=False,
|
||||
can_assign_tasks=False,
|
||||
order=1,
|
||||
parent_id=None,
|
||||
view_style="list",
|
||||
)
|
||||
]
|
||||
api.get_sections.return_value = [
|
||||
Section(
|
||||
id=SECTION_ID,
|
||||
project_id=PROJECT_ID,
|
||||
name="Section Name",
|
||||
order=1,
|
||||
)
|
||||
]
|
||||
api.get_labels.return_value = [
|
||||
Label(id="1", name="Label1", color="1", order=1, is_favorite=False)
|
||||
]
|
||||
api.get_collaborators.return_value = [
|
||||
Collaborator(email="user@gmail.com", id="1", name="user")
|
||||
]
|
||||
api.get_tasks.return_value = tasks
|
||||
api.get_projects.side_effect = make_api_response(
|
||||
[
|
||||
Project(
|
||||
id=PROJECT_ID,
|
||||
color="blue",
|
||||
is_favorite=False,
|
||||
name="Name",
|
||||
is_shared=False,
|
||||
is_archived=False,
|
||||
is_collapsed=False,
|
||||
is_inbox_project=False,
|
||||
can_assign_tasks=False,
|
||||
order=1,
|
||||
parent_id=None,
|
||||
view_style="list",
|
||||
description="",
|
||||
created_at="2021-01-01",
|
||||
updated_at="2021-01-01",
|
||||
)
|
||||
]
|
||||
)
|
||||
api.get_sections.side_effect = make_api_response(
|
||||
[
|
||||
Section(
|
||||
id=SECTION_ID,
|
||||
project_id=PROJECT_ID,
|
||||
name="Section Name",
|
||||
order=1,
|
||||
is_collapsed=False,
|
||||
)
|
||||
]
|
||||
)
|
||||
api.get_labels.side_effect = make_api_response(
|
||||
[Label(id="1", name="Label1", color="1", order=1, is_favorite=False)]
|
||||
)
|
||||
api.get_collaborators.side_effect = make_api_response(
|
||||
[Collaborator(email="user@gmail.com", id="1", name="user")]
|
||||
)
|
||||
api.get_tasks.side_effect = make_api_response(tasks)
|
||||
return api
|
||||
|
||||
|
||||
|
||||
@@ -29,7 +29,7 @@ from homeassistant.helpers import entity_registry as er
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .conftest import PROJECT_ID, SECTION_ID, SUMMARY
|
||||
from .conftest import PROJECT_ID, SECTION_ID, SUMMARY, make_api_due
|
||||
|
||||
from tests.typing import ClientSessionGenerator
|
||||
|
||||
@@ -147,7 +147,7 @@ async def test_update_entity_for_custom_project_no_due_date_on(
|
||||
@pytest.mark.parametrize(
|
||||
"due",
|
||||
[
|
||||
Due(
|
||||
make_api_due(
|
||||
# Note: This runs before the test fixture that sets the timezone
|
||||
date=(
|
||||
datetime(
|
||||
@@ -164,6 +164,7 @@ async def test_update_entity_for_calendar_with_due_date_in_the_future(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
api: AsyncMock,
|
||||
due: Due,
|
||||
) -> 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")
|
||||
@@ -216,37 +217,37 @@ async def test_calendar_custom_project_unique_id(
|
||||
("due", "start", "end", "expected_response"),
|
||||
[
|
||||
(
|
||||
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
make_api_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"})],
|
||||
),
|
||||
(
|
||||
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
make_api_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"),
|
||||
make_api_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"),
|
||||
make_api_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"),
|
||||
make_api_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"),
|
||||
make_api_due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
"2023-03-29T06:00:00.000Z",
|
||||
"2023-03-30T06:00:00.000Z",
|
||||
[],
|
||||
@@ -334,25 +335,22 @@ async def test_create_task_service_call_with_section(
|
||||
[
|
||||
# These are all equivalent due dates for the same time in different
|
||||
# timezone formats.
|
||||
Due(
|
||||
date="2023-03-30",
|
||||
make_api_due(
|
||||
date="2023-03-31T00:00:00Z",
|
||||
is_recurring=False,
|
||||
string="Mar 30 6:00 PM",
|
||||
datetime="2023-03-31T00:00:00Z",
|
||||
timezone="America/Regina",
|
||||
),
|
||||
Due(
|
||||
date="2023-03-30",
|
||||
make_api_due(
|
||||
date="2023-03-31T00:00:00Z",
|
||||
is_recurring=False,
|
||||
string="Mar 30 7:00 PM",
|
||||
datetime="2023-03-31T00:00:00Z",
|
||||
timezone="America/Los_Angeles",
|
||||
),
|
||||
Due(
|
||||
date="2023-03-30",
|
||||
make_api_due(
|
||||
date="2023-03-30T18:00:00",
|
||||
is_recurring=False,
|
||||
string="Mar 30 6:00 PM",
|
||||
datetime="2023-03-30T18:00:00",
|
||||
),
|
||||
],
|
||||
ids=("in_local_timezone", "in_other_timezone", "floating"),
|
||||
@@ -431,35 +429,35 @@ async def test_task_due_datetime(
|
||||
[
|
||||
(
|
||||
{"custom_projects": [{"name": "Test", "labels": ["Label1"]}]},
|
||||
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
make_api_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"})],
|
||||
),
|
||||
(
|
||||
{"custom_projects": [{"name": "Test", "labels": ["custom"]}]},
|
||||
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
make_api_due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
"2023-03-28T00:00:00.000Z",
|
||||
"2023-04-01T00:00:00.000Z",
|
||||
[],
|
||||
),
|
||||
(
|
||||
{"custom_projects": [{"name": "Test", "include_projects": ["Name"]}]},
|
||||
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
make_api_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"})],
|
||||
),
|
||||
(
|
||||
{"custom_projects": [{"name": "Test", "due_date_days": 1}]},
|
||||
Due(date="2023-03-30", is_recurring=False, string="Mar 30"),
|
||||
make_api_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"})],
|
||||
),
|
||||
(
|
||||
{"custom_projects": [{"name": "Test", "due_date_days": 1}]},
|
||||
Due(
|
||||
make_api_due(
|
||||
date=(dt_util.now() + timedelta(days=2)).strftime("%Y-%m-%d"),
|
||||
is_recurring=False,
|
||||
string="Mar 30",
|
||||
@@ -497,11 +495,10 @@ async def test_events_filtered_for_custom_projects(
|
||||
("due", "setup_platform"),
|
||||
[
|
||||
(
|
||||
Due(
|
||||
date="2023-03-30",
|
||||
make_api_due(
|
||||
date="2023-03-31T00:00:00Z",
|
||||
is_recurring=False,
|
||||
string="Mar 30 6:00 PM",
|
||||
datetime="2023-03-31T00:00:00Z",
|
||||
timezone="America/Regina",
|
||||
),
|
||||
None,
|
||||
|
||||
@@ -4,7 +4,7 @@ from typing import Any
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import pytest
|
||||
from todoist_api_python.models import Due, Task
|
||||
from todoist_api_python.models import Task
|
||||
|
||||
from homeassistant.components.todo import (
|
||||
ATTR_DESCRIPTION,
|
||||
@@ -20,7 +20,7 @@ from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.entity_component import async_update_entity
|
||||
|
||||
from .conftest import PROJECT_ID, make_api_task
|
||||
from .conftest import PROJECT_ID, make_api_due, make_api_response, make_api_task
|
||||
|
||||
from tests.typing import WebSocketGenerator
|
||||
|
||||
@@ -41,12 +41,19 @@ async def set_time_zone(hass: HomeAssistant) -> None:
|
||||
("tasks", "expected_state"),
|
||||
[
|
||||
([], "0"),
|
||||
([make_api_task(id="12345", content="Soda", is_completed=False)], "1"),
|
||||
([make_api_task(id="12345", content="Soda", is_completed=True)], "0"),
|
||||
([make_api_task(id="12345", content="Soda", completed_at=None)], "1"),
|
||||
(
|
||||
[
|
||||
make_api_task(id="12345", content="Milk", is_completed=False),
|
||||
make_api_task(id="54321", content="Soda", is_completed=False),
|
||||
make_api_task(
|
||||
id="12345", content="Soda", completed_at="2021-10-01T00:00:00"
|
||||
)
|
||||
],
|
||||
"0",
|
||||
),
|
||||
(
|
||||
[
|
||||
make_api_task(id="12345", content="Milk", completed_at=None),
|
||||
make_api_task(id="54321", content="Soda", completed_at=None),
|
||||
],
|
||||
"2",
|
||||
),
|
||||
@@ -55,7 +62,7 @@ async def set_time_zone(hass: HomeAssistant) -> None:
|
||||
make_api_task(
|
||||
id="12345",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
project_id="other-project-id",
|
||||
)
|
||||
],
|
||||
@@ -64,7 +71,7 @@ async def set_time_zone(hass: HomeAssistant) -> None:
|
||||
(
|
||||
[
|
||||
make_api_task(
|
||||
id="12345", content="sub-task", is_completed=False, parent_id="1"
|
||||
id="12345", content="sub-task", completed_at=None, parent_id="1"
|
||||
)
|
||||
],
|
||||
"0",
|
||||
@@ -89,7 +96,7 @@ async def test_todo_item_state(
|
||||
(
|
||||
[],
|
||||
{},
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
[make_api_task(id="task-id-1", content="Soda", completed_at=None)],
|
||||
{"content": "Soda", "due_string": "no date", "description": ""},
|
||||
{"uid": "task-id-1", "summary": "Soda", "status": "needs_action"},
|
||||
),
|
||||
@@ -100,8 +107,10 @@ async def test_todo_item_state(
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
||||
completed_at=None,
|
||||
due=make_api_due(
|
||||
date="2023-11-18", is_recurring=False, string="today"
|
||||
),
|
||||
)
|
||||
],
|
||||
{"description": "", "due_date": "2023-11-18"},
|
||||
@@ -119,11 +128,10 @@ async def test_todo_item_state(
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(
|
||||
date="2023-11-18",
|
||||
completed_at=None,
|
||||
due=make_api_due(
|
||||
date="2023-11-18T12:30:00.000000Z",
|
||||
is_recurring=False,
|
||||
datetime="2023-11-18T12:30:00.000000Z",
|
||||
string="today",
|
||||
),
|
||||
)
|
||||
@@ -147,7 +155,7 @@ async def test_todo_item_state(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
)
|
||||
],
|
||||
{"description": "6-pack", "due_string": "no date"},
|
||||
@@ -178,7 +186,7 @@ async def test_add_todo_list_item(
|
||||
|
||||
api.add_task = AsyncMock()
|
||||
# Fake API response when state is refreshed after create
|
||||
api.get_tasks.return_value = tasks_after_update
|
||||
api.get_tasks.side_effect = make_api_response(tasks_after_update)
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
@@ -209,7 +217,7 @@ async def test_add_todo_list_item(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tasks"), [[make_api_task(id="task-id-1", content="Soda", is_completed=False)]]
|
||||
("tasks"), [[make_api_task(id="task-id-1", content="Soda", completed_at=None)]]
|
||||
)
|
||||
async def test_update_todo_item_status(
|
||||
hass: HomeAssistant,
|
||||
@@ -222,13 +230,17 @@ async def test_update_todo_item_status(
|
||||
assert state
|
||||
assert state.state == "1"
|
||||
|
||||
api.close_task = AsyncMock()
|
||||
api.reopen_task = AsyncMock()
|
||||
api.complete_task = AsyncMock()
|
||||
api.uncomplete_task = AsyncMock()
|
||||
|
||||
# Fake API response when state is refreshed after close
|
||||
api.get_tasks.return_value = [
|
||||
make_api_task(id="task-id-1", content="Soda", is_completed=True)
|
||||
]
|
||||
# Fake API response when state is refreshed after complete
|
||||
api.get_tasks.side_effect = make_api_response(
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1", content="Soda", completed_at="2021-10-01T00:00:00"
|
||||
)
|
||||
]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
@@ -237,21 +249,21 @@ async def test_update_todo_item_status(
|
||||
target={ATTR_ENTITY_ID: "todo.name"},
|
||||
blocking=True,
|
||||
)
|
||||
assert api.close_task.called
|
||||
args = api.close_task.call_args
|
||||
assert api.complete_task.called
|
||||
args = api.complete_task.call_args
|
||||
assert args
|
||||
assert args.kwargs.get("task_id") == "task-id-1"
|
||||
assert not api.reopen_task.called
|
||||
assert not api.uncomplete_task.called
|
||||
|
||||
# Verify state is refreshed
|
||||
state = hass.states.get("todo.name")
|
||||
assert state
|
||||
assert state.state == "0"
|
||||
|
||||
# Fake API response when state is refreshed after reopen
|
||||
api.get_tasks.return_value = [
|
||||
make_api_task(id="task-id-1", content="Soda", is_completed=False)
|
||||
]
|
||||
# Fake API response when state is refreshed after reopening task
|
||||
api.get_tasks.side_effect = make_api_response(
|
||||
[make_api_task(id="task-id-1", content="Soda", completed_at=None)]
|
||||
)
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
@@ -260,8 +272,8 @@ async def test_update_todo_item_status(
|
||||
target={ATTR_ENTITY_ID: "todo.name"},
|
||||
blocking=True,
|
||||
)
|
||||
assert api.reopen_task.called
|
||||
args = api.reopen_task.call_args
|
||||
assert api.uncomplete_task.called
|
||||
args = api.uncomplete_task.call_args
|
||||
assert args
|
||||
assert args.kwargs.get("task_id") == "task-id-1"
|
||||
|
||||
@@ -279,7 +291,7 @@ async def test_update_todo_item_status(
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
description="desc",
|
||||
)
|
||||
],
|
||||
@@ -288,7 +300,7 @@ async def test_update_todo_item_status(
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Milk",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
description="desc",
|
||||
)
|
||||
],
|
||||
@@ -306,14 +318,16 @@ async def test_update_todo_item_status(
|
||||
},
|
||||
),
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
[make_api_task(id="task-id-1", content="Soda", completed_at=None)],
|
||||
{ATTR_DUE_DATE: "2023-11-18"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(is_recurring=False, date="2023-11-18", string="today"),
|
||||
completed_at=None,
|
||||
due=make_api_due(
|
||||
date="2023-11-18", is_recurring=False, string="today"
|
||||
),
|
||||
)
|
||||
],
|
||||
{
|
||||
@@ -330,17 +344,16 @@ async def test_update_todo_item_status(
|
||||
},
|
||||
),
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
[make_api_task(id="task-id-1", content="Soda", completed_at=None)],
|
||||
{ATTR_DUE_DATETIME: "2023-11-18T06:30:00"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
due=Due(
|
||||
date="2023-11-18",
|
||||
completed_at=None,
|
||||
due=make_api_due(
|
||||
date="2023-11-18T12:30:00.000000Z",
|
||||
is_recurring=False,
|
||||
datetime="2023-11-18T12:30:00.000000Z",
|
||||
string="today",
|
||||
),
|
||||
)
|
||||
@@ -359,14 +372,14 @@ async def test_update_todo_item_status(
|
||||
},
|
||||
),
|
||||
(
|
||||
[make_api_task(id="task-id-1", content="Soda", is_completed=False)],
|
||||
[make_api_task(id="task-id-1", content="Soda", completed_at=None)],
|
||||
{ATTR_DESCRIPTION: "6-pack"},
|
||||
[
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
)
|
||||
],
|
||||
{
|
||||
@@ -388,7 +401,7 @@ async def test_update_todo_item_status(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
)
|
||||
],
|
||||
{ATTR_DESCRIPTION: None},
|
||||
@@ -396,7 +409,7 @@ async def test_update_todo_item_status(
|
||||
make_api_task(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
is_completed=False,
|
||||
completed_at=None,
|
||||
description="",
|
||||
)
|
||||
],
|
||||
@@ -418,10 +431,12 @@ async def test_update_todo_item_status(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
# Create a mock task with a string value in the Due object and verify it
|
||||
completed_at=None,
|
||||
# Create a mock task with a Due object and verify the due string
|
||||
# gets preserved when verifying the kwargs to update below
|
||||
due=Due(date="2024-01-01", is_recurring=True, string="every day"),
|
||||
due=make_api_due(
|
||||
date="2024-01-01", is_recurring=True, string="every day"
|
||||
),
|
||||
)
|
||||
],
|
||||
{ATTR_DUE_DATE: "2024-02-01"},
|
||||
@@ -430,8 +445,10 @@ async def test_update_todo_item_status(
|
||||
id="task-id-1",
|
||||
content="Soda",
|
||||
description="6-pack",
|
||||
is_completed=False,
|
||||
due=Due(date="2024-02-01", is_recurring=True, string="every day"),
|
||||
completed_at=None,
|
||||
due=make_api_due(
|
||||
date="2024-02-01", is_recurring=True, string="every day"
|
||||
),
|
||||
)
|
||||
],
|
||||
{
|
||||
@@ -477,7 +494,7 @@ async def test_update_todo_items(
|
||||
api.update_task = AsyncMock()
|
||||
|
||||
# Fake API response when state is refreshed after close
|
||||
api.get_tasks.return_value = tasks_after_update
|
||||
api.get_tasks.side_effect = make_api_response(tasks_after_update)
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
@@ -506,8 +523,8 @@ async def test_update_todo_items(
|
||||
("tasks"),
|
||||
[
|
||||
[
|
||||
make_api_task(id="task-id-1", content="Soda", is_completed=False),
|
||||
make_api_task(id="task-id-2", content="Milk", is_completed=False),
|
||||
make_api_task(id="task-id-1", content="Soda", completed_at=None),
|
||||
make_api_task(id="task-id-2", content="Milk", completed_at=None),
|
||||
]
|
||||
],
|
||||
)
|
||||
@@ -524,7 +541,7 @@ async def test_remove_todo_item(
|
||||
|
||||
api.delete_task = AsyncMock()
|
||||
# Fake API response when state is refreshed after close
|
||||
api.get_tasks.return_value = []
|
||||
api.get_tasks.side_effect = make_api_response([])
|
||||
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
@@ -545,7 +562,7 @@ async def test_remove_todo_item(
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("tasks"), [[make_api_task(id="task-id-1", content="Cheese", is_completed=False)]]
|
||||
("tasks"), [[make_api_task(id="task-id-1", content="Cheese", completed_at=None)]]
|
||||
)
|
||||
async def test_subscribe(
|
||||
hass: HomeAssistant,
|
||||
@@ -579,9 +596,9 @@ async def test_subscribe(
|
||||
assert items[0]["uid"]
|
||||
|
||||
# Fake API response when state is refreshed
|
||||
api.get_tasks.return_value = [
|
||||
make_api_task(id="test-id-1", content="Wine", is_completed=False)
|
||||
]
|
||||
api.get_tasks.side_effect = make_api_response(
|
||||
[make_api_task(id="test-id-1", content="Wine", completed_at=None)]
|
||||
)
|
||||
await hass.services.async_call(
|
||||
TODO_DOMAIN,
|
||||
TodoServices.UPDATE_ITEM,
|
||||
|
||||
Reference in New Issue
Block a user