mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 13:47:35 +00:00
Add CalDAV To-do item support for Add, Update, and Delete (#103922)
* Add CalDAV To-do item support for Add, Update, and Delete * Remove unnecessary cast * Fix ruff error * Fix ruff errors * Remove exception from error message * Remove unnecessary duplicate state update
This commit is contained in:
parent
422b09f4ec
commit
613afe322f
@ -1,15 +1,25 @@
|
|||||||
"""CalDAV todo platform."""
|
"""CalDAV todo platform."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from typing import cast
|
||||||
|
|
||||||
import caldav
|
import caldav
|
||||||
|
from caldav.lib.error import DAVError, NotFoundError
|
||||||
|
import requests
|
||||||
|
|
||||||
from homeassistant.components.todo import TodoItem, TodoItemStatus, TodoListEntity
|
from homeassistant.components.todo import (
|
||||||
|
TodoItem,
|
||||||
|
TodoItemStatus,
|
||||||
|
TodoListEntity,
|
||||||
|
TodoListEntityFeature,
|
||||||
|
)
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||||
|
|
||||||
from .api import async_get_calendars, get_attr_value
|
from .api import async_get_calendars, get_attr_value
|
||||||
@ -26,6 +36,10 @@ TODO_STATUS_MAP = {
|
|||||||
"COMPLETED": TodoItemStatus.COMPLETED,
|
"COMPLETED": TodoItemStatus.COMPLETED,
|
||||||
"CANCELLED": TodoItemStatus.COMPLETED,
|
"CANCELLED": TodoItemStatus.COMPLETED,
|
||||||
}
|
}
|
||||||
|
TODO_STATUS_MAP_INV: dict[TodoItemStatus, str] = {
|
||||||
|
TodoItemStatus.NEEDS_ACTION: "NEEDS-ACTION",
|
||||||
|
TodoItemStatus.COMPLETED: "COMPLETED",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
async def async_setup_entry(
|
async def async_setup_entry(
|
||||||
@ -71,6 +85,11 @@ class WebDavTodoListEntity(TodoListEntity):
|
|||||||
"""CalDAV To-do list entity."""
|
"""CalDAV To-do list entity."""
|
||||||
|
|
||||||
_attr_has_entity_name = True
|
_attr_has_entity_name = True
|
||||||
|
_attr_supported_features = (
|
||||||
|
TodoListEntityFeature.CREATE_TODO_ITEM
|
||||||
|
| TodoListEntityFeature.UPDATE_TODO_ITEM
|
||||||
|
| TodoListEntityFeature.DELETE_TODO_ITEM
|
||||||
|
)
|
||||||
|
|
||||||
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
|
def __init__(self, calendar: caldav.Calendar, config_entry_id: str) -> None:
|
||||||
"""Initialize WebDavTodoListEntity."""
|
"""Initialize WebDavTodoListEntity."""
|
||||||
@ -92,3 +111,66 @@ class WebDavTodoListEntity(TodoListEntity):
|
|||||||
for resource in results
|
for resource in results
|
||||||
if (todo_item := _todo_item(resource)) is not None
|
if (todo_item := _todo_item(resource)) is not None
|
||||||
]
|
]
|
||||||
|
|
||||||
|
async def async_create_todo_item(self, item: TodoItem) -> None:
|
||||||
|
"""Add an item to the To-do list."""
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
partial(
|
||||||
|
self._calendar.save_todo,
|
||||||
|
summary=item.summary,
|
||||||
|
status=TODO_STATUS_MAP_INV.get(
|
||||||
|
item.status or TodoItemStatus.NEEDS_ACTION, "NEEDS-ACTION"
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
|
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||||
|
|
||||||
|
async def async_update_todo_item(self, item: TodoItem) -> None:
|
||||||
|
"""Update a To-do item."""
|
||||||
|
uid: str = cast(str, item.uid)
|
||||||
|
try:
|
||||||
|
todo = await self.hass.async_add_executor_job(
|
||||||
|
self._calendar.todo_by_uid, uid
|
||||||
|
)
|
||||||
|
except NotFoundError as err:
|
||||||
|
raise HomeAssistantError(f"Could not find To-do item {uid}") from err
|
||||||
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
|
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||||
|
vtodo = todo.icalendar_component # type: ignore[attr-defined]
|
||||||
|
if item.summary:
|
||||||
|
vtodo["summary"] = item.summary
|
||||||
|
if item.status:
|
||||||
|
vtodo["status"] = TODO_STATUS_MAP_INV.get(item.status, "NEEDS-ACTION")
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(
|
||||||
|
partial(
|
||||||
|
todo.save,
|
||||||
|
no_create=True,
|
||||||
|
obj_type="todo",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
|
raise HomeAssistantError(f"CalDAV save error: {err}") from err
|
||||||
|
|
||||||
|
async def async_delete_todo_items(self, uids: list[str]) -> None:
|
||||||
|
"""Delete To-do items."""
|
||||||
|
tasks = (
|
||||||
|
self.hass.async_add_executor_job(self._calendar.todo_by_uid, uid)
|
||||||
|
for uid in uids
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
items = await asyncio.gather(*tasks)
|
||||||
|
except NotFoundError as err:
|
||||||
|
raise HomeAssistantError("Could not find To-do item") from err
|
||||||
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
|
raise HomeAssistantError(f"CalDAV lookup error: {err}") from err
|
||||||
|
|
||||||
|
# Run serially as some CalDAV servers do not support concurrent modifications
|
||||||
|
for item in items:
|
||||||
|
try:
|
||||||
|
await self.hass.async_add_executor_job(item.delete)
|
||||||
|
except (requests.ConnectionError, DAVError) as err:
|
||||||
|
raise HomeAssistantError(f"CalDAV delete error: {err}") from err
|
||||||
|
@ -1,17 +1,22 @@
|
|||||||
"""The tests for the webdav todo component."""
|
"""The tests for the webdav todo component."""
|
||||||
|
from typing import Any
|
||||||
from unittest.mock import MagicMock, Mock
|
from unittest.mock import MagicMock, Mock
|
||||||
|
|
||||||
|
from caldav.lib.error import DAVError, NotFoundError
|
||||||
from caldav.objects import Todo
|
from caldav.objects import Todo
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.todo import DOMAIN as TODO_DOMAIN
|
||||||
from homeassistant.const import Platform
|
from homeassistant.const import Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
|
|
||||||
from tests.common import MockConfigEntry
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
CALENDAR_NAME = "My Tasks"
|
CALENDAR_NAME = "My Tasks"
|
||||||
ENTITY_NAME = "My tasks"
|
ENTITY_NAME = "My tasks"
|
||||||
TEST_ENTITY = "todo.my_tasks"
|
TEST_ENTITY = "todo.my_tasks"
|
||||||
|
SUPPORTED_FEATURES = 7
|
||||||
|
|
||||||
TODO_NO_STATUS = """BEGIN:VCALENDAR
|
TODO_NO_STATUS = """BEGIN:VCALENDAR
|
||||||
VERSION:2.0
|
VERSION:2.0
|
||||||
@ -75,17 +80,32 @@ def mock_supported_components() -> list[str]:
|
|||||||
return ["VTODO"]
|
return ["VTODO"]
|
||||||
|
|
||||||
|
|
||||||
@pytest.fixture(name="calendars")
|
@pytest.fixture(name="calendar")
|
||||||
def mock_calendars(todos: list[str], supported_components: list[str]) -> list[Mock]:
|
def mock_calendar(supported_components: list[str]) -> Mock:
|
||||||
"""Fixture to create calendars for the test."""
|
"""Fixture to create the primary calendar for the test."""
|
||||||
calendar = Mock()
|
calendar = Mock()
|
||||||
items = [
|
calendar.search = MagicMock(return_value=[])
|
||||||
Todo(None, f"{idx}.ics", item, calendar, str(idx))
|
|
||||||
for idx, item in enumerate(todos)
|
|
||||||
]
|
|
||||||
calendar.search = MagicMock(return_value=items)
|
|
||||||
calendar.name = CALENDAR_NAME
|
calendar.name = CALENDAR_NAME
|
||||||
calendar.get_supported_components = MagicMock(return_value=supported_components)
|
calendar.get_supported_components = MagicMock(return_value=supported_components)
|
||||||
|
return calendar
|
||||||
|
|
||||||
|
|
||||||
|
def create_todo(calendar: Mock, idx: str, ics: str) -> Todo:
|
||||||
|
"""Create a caldav Todo object."""
|
||||||
|
return Todo(client=None, url=f"{idx}.ics", data=ics, parent=calendar, id=idx)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(autouse=True)
|
||||||
|
def mock_search_items(calendar: Mock, todos: list[str]) -> None:
|
||||||
|
"""Fixture to add search results to the test calendar."""
|
||||||
|
calendar.search.return_value = [
|
||||||
|
create_todo(calendar, str(idx), item) for idx, item in enumerate(todos)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture(name="calendars")
|
||||||
|
def mock_calendars(calendar: Mock) -> list[Mock]:
|
||||||
|
"""Fixture to create calendars for the test."""
|
||||||
return [calendar]
|
return [calendar]
|
||||||
|
|
||||||
|
|
||||||
@ -137,6 +157,7 @@ async def test_todo_list_state(
|
|||||||
assert state.state == expected_state
|
assert state.state == expected_state
|
||||||
assert dict(state.attributes) == {
|
assert dict(state.attributes) == {
|
||||||
"friendly_name": ENTITY_NAME,
|
"friendly_name": ENTITY_NAME,
|
||||||
|
"supported_features": SUPPORTED_FEATURES,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@ -154,3 +175,324 @@ async def test_supported_components(
|
|||||||
|
|
||||||
state = hass.states.get(TEST_ENTITY)
|
state = hass.states.get(TEST_ENTITY)
|
||||||
assert (state is not None) == has_entity
|
assert (state is not None) == has_entity
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_item(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
calendar: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test adding an item to the list."""
|
||||||
|
calendar.search.return_value = []
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY)
|
||||||
|
assert state
|
||||||
|
assert state.state == "0"
|
||||||
|
|
||||||
|
# Simulat return value for the state update after the service call
|
||||||
|
calendar.search.return_value = [create_todo(calendar, "2", TODO_NEEDS_ACTION)]
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"add_item",
|
||||||
|
{"item": "Cheese"},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert calendar.save_todo.call_args
|
||||||
|
assert calendar.save_todo.call_args.kwargs == {
|
||||||
|
"status": "NEEDS-ACTION",
|
||||||
|
"summary": "Cheese",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Verify state was updated
|
||||||
|
state = hass.states.get(TEST_ENTITY)
|
||||||
|
assert state
|
||||||
|
assert state.state == "1"
|
||||||
|
|
||||||
|
|
||||||
|
async def test_add_item_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
calendar: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test failure when adding an item to the list."""
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
calendar.save_todo.side_effect = DAVError()
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="CalDAV save error"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"add_item",
|
||||||
|
{"item": "Cheese"},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("update_data", "expected_ics", "expected_state"),
|
||||||
|
[
|
||||||
|
(
|
||||||
|
{"rename": "Swiss Cheese"},
|
||||||
|
["SUMMARY:Swiss Cheese", "STATUS:NEEDS-ACTION"],
|
||||||
|
"1",
|
||||||
|
),
|
||||||
|
({"status": "needs_action"}, ["SUMMARY:Cheese", "STATUS:NEEDS-ACTION"], "1"),
|
||||||
|
({"status": "completed"}, ["SUMMARY:Cheese", "STATUS:COMPLETED"], "0"),
|
||||||
|
(
|
||||||
|
{"rename": "Swiss Cheese", "status": "needs_action"},
|
||||||
|
["SUMMARY:Swiss Cheese", "STATUS:NEEDS-ACTION"],
|
||||||
|
"1",
|
||||||
|
),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_update_item(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
dav_client: Mock,
|
||||||
|
calendar: Mock,
|
||||||
|
update_data: dict[str, Any],
|
||||||
|
expected_ics: list[str],
|
||||||
|
expected_state: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test creating a an item on the list."""
|
||||||
|
|
||||||
|
item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2")
|
||||||
|
calendar.search = MagicMock(return_value=[item])
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY)
|
||||||
|
assert state
|
||||||
|
assert state.state == "1"
|
||||||
|
|
||||||
|
calendar.todo_by_uid = MagicMock(return_value=item)
|
||||||
|
|
||||||
|
dav_client.put.return_value.status = 204
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{
|
||||||
|
"item": "Cheese",
|
||||||
|
**update_data,
|
||||||
|
},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert dav_client.put.call_args
|
||||||
|
ics = dav_client.put.call_args.args[1]
|
||||||
|
for expected in expected_ics:
|
||||||
|
assert expected in ics
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY)
|
||||||
|
assert state
|
||||||
|
assert state.state == expected_state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_update_item_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
dav_client: Mock,
|
||||||
|
calendar: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test failure when updating an item on the list."""
|
||||||
|
|
||||||
|
item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2")
|
||||||
|
calendar.search = MagicMock(return_value=[item])
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
calendar.todo_by_uid = MagicMock(return_value=item)
|
||||||
|
dav_client.put.side_effect = DAVError()
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="CalDAV save error"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{
|
||||||
|
"item": "Cheese",
|
||||||
|
"status": "completed",
|
||||||
|
},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("side_effect", "match"),
|
||||||
|
[(DAVError, "CalDAV lookup error"), (NotFoundError, "Could not find")],
|
||||||
|
)
|
||||||
|
async def test_update_item_lookup_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
dav_client: Mock,
|
||||||
|
calendar: Mock,
|
||||||
|
side_effect: Any,
|
||||||
|
match: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test failure when looking up an item to update."""
|
||||||
|
|
||||||
|
item = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2")
|
||||||
|
calendar.search = MagicMock(return_value=[item])
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
calendar.todo_by_uid.side_effect = side_effect
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match=match):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"update_item",
|
||||||
|
{
|
||||||
|
"item": "Cheese",
|
||||||
|
"status": "completed",
|
||||||
|
},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("uids_to_delete", "expect_item1_delete_called", "expect_item2_delete_called"),
|
||||||
|
[
|
||||||
|
([], False, False),
|
||||||
|
(["Cheese"], True, False),
|
||||||
|
(["Wine"], False, True),
|
||||||
|
(["Wine", "Cheese"], True, True),
|
||||||
|
],
|
||||||
|
ids=("none", "item1-only", "item2-only", "both-items"),
|
||||||
|
)
|
||||||
|
async def test_remove_item(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
dav_client: Mock,
|
||||||
|
calendar: Mock,
|
||||||
|
uids_to_delete: list[str],
|
||||||
|
expect_item1_delete_called: bool,
|
||||||
|
expect_item2_delete_called: bool,
|
||||||
|
) -> None:
|
||||||
|
"""Test removing an item on the list."""
|
||||||
|
|
||||||
|
item1 = Todo(dav_client, None, TODO_NEEDS_ACTION, calendar, "2")
|
||||||
|
item2 = Todo(dav_client, None, TODO_COMPLETED, calendar, "3")
|
||||||
|
calendar.search = MagicMock(return_value=[item1, item2])
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
state = hass.states.get(TEST_ENTITY)
|
||||||
|
assert state
|
||||||
|
assert state.state == "1"
|
||||||
|
|
||||||
|
def lookup(uid: str) -> Mock:
|
||||||
|
assert uid == "2" or uid == "3"
|
||||||
|
if uid == "2":
|
||||||
|
return item1
|
||||||
|
return item2
|
||||||
|
|
||||||
|
calendar.todo_by_uid = Mock(side_effect=lookup)
|
||||||
|
item1.delete = Mock()
|
||||||
|
item2.delete = Mock()
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": uids_to_delete},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert item1.delete.called == expect_item1_delete_called
|
||||||
|
assert item2.delete.called == expect_item2_delete_called
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("todos", "side_effect", "match"),
|
||||||
|
[
|
||||||
|
([TODO_NEEDS_ACTION], DAVError, "CalDAV lookup error"),
|
||||||
|
([TODO_NEEDS_ACTION], NotFoundError, "Could not find"),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_remove_item_lookup_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
calendar: Mock,
|
||||||
|
side_effect: Any,
|
||||||
|
match: str,
|
||||||
|
) -> None:
|
||||||
|
"""Test failure while removing an item from the list."""
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
calendar.todo_by_uid.side_effect = side_effect
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match=match):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": "Cheese"},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_item_failure(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
dav_client: Mock,
|
||||||
|
calendar: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test removing an item on the list."""
|
||||||
|
|
||||||
|
item = Todo(dav_client, "2.ics", TODO_NEEDS_ACTION, calendar, "2")
|
||||||
|
calendar.search = MagicMock(return_value=[item])
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
def lookup(uid: str) -> Mock:
|
||||||
|
return item
|
||||||
|
|
||||||
|
calendar.todo_by_uid = Mock(side_effect=lookup)
|
||||||
|
dav_client.delete.return_value.status = 500
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="CalDAV delete error"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": "Cheese"},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_remove_item_not_found(
|
||||||
|
hass: HomeAssistant,
|
||||||
|
config_entry: MockConfigEntry,
|
||||||
|
dav_client: Mock,
|
||||||
|
calendar: Mock,
|
||||||
|
) -> None:
|
||||||
|
"""Test removing an item on the list."""
|
||||||
|
|
||||||
|
item = Todo(dav_client, "2.ics", TODO_NEEDS_ACTION, calendar, "2")
|
||||||
|
calendar.search = MagicMock(return_value=[item])
|
||||||
|
|
||||||
|
await config_entry.async_setup(hass)
|
||||||
|
|
||||||
|
def lookup(uid: str) -> Mock:
|
||||||
|
return item
|
||||||
|
|
||||||
|
calendar.todo_by_uid.side_effect = NotFoundError()
|
||||||
|
|
||||||
|
with pytest.raises(HomeAssistantError, match="Could not find"):
|
||||||
|
await hass.services.async_call(
|
||||||
|
TODO_DOMAIN,
|
||||||
|
"remove_item",
|
||||||
|
{"item": "Cheese"},
|
||||||
|
target={"entity_id": TEST_ENTITY},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user