core/tests/components/todo/test_init.py

1304 lines
36 KiB
Python
Raw Normal View History

2023-10-23 20:53:00 +00:00
"""Tests for the todo integration."""
from collections.abc import Generator
import datetime
2023-10-23 20:53:00 +00:00
from typing import Any
from unittest.mock import AsyncMock
import zoneinfo
2023-10-23 20:53:00 +00:00
import pytest
import voluptuous as vol
from homeassistant.components.todo import (
DOMAIN,
TodoItem,
TodoItemStatus,
TodoListEntity,
TodoListEntityFeature,
intent as todo_intent,
2023-10-23 20:53:00 +00:00
)
from homeassistant.config_entries import ConfigEntry, ConfigEntryState, ConfigFlow
from homeassistant.const import Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers import intent
2023-10-23 20:53:00 +00:00
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from tests.common import (
MockConfigEntry,
MockModule,
MockPlatform,
mock_config_flow,
mock_integration,
mock_platform,
)
from tests.typing import WebSocketGenerator
TEST_DOMAIN = "test"
ITEM_1 = {
"uid": "1",
"summary": "Item #1",
"status": "needs_action",
}
ITEM_2 = {
"uid": "2",
"summary": "Item #2",
"status": "completed",
}
TEST_TIMEZONE = zoneinfo.ZoneInfo("America/Regina")
TEST_OFFSET = "-06:00"
2023-10-23 20:53:00 +00:00
class MockFlow(ConfigFlow):
"""Test flow."""
class MockTodoListEntity(TodoListEntity):
"""Test todo list entity."""
def __init__(self, items: list[TodoItem] | None = None) -> None:
"""Initialize entity."""
self._attr_todo_items = items or []
@property
def items(self) -> list[TodoItem]:
"""Return the items in the To-do list."""
return self._attr_todo_items
async def async_create_todo_item(self, item: TodoItem) -> None:
"""Add an item to the To-do list."""
self._attr_todo_items.append(item)
async def async_delete_todo_items(self, uids: list[str]) -> None:
"""Delete an item in the To-do list."""
self._attr_todo_items = [item for item in self.items if item.uid not in uids]
2023-10-23 20:53:00 +00:00
@pytest.fixture(autouse=True)
def config_flow_fixture(hass: HomeAssistant) -> Generator[None, None, None]:
"""Mock config flow."""
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
with mock_config_flow(TEST_DOMAIN, MockFlow):
yield
@pytest.fixture(autouse=True)
def mock_setup_integration(hass: HomeAssistant) -> None:
"""Fixture to set up a mock integration."""
async def async_setup_entry_init(
hass: HomeAssistant, config_entry: ConfigEntry
) -> bool:
"""Set up test config entry."""
await hass.config_entries.async_forward_entry_setup(config_entry, DOMAIN)
return True
async def async_unload_entry_init(
hass: HomeAssistant,
config_entry: ConfigEntry,
) -> bool:
await hass.config_entries.async_unload_platforms(config_entry, [Platform.TODO])
return True
mock_platform(hass, f"{TEST_DOMAIN}.config_flow")
mock_integration(
hass,
MockModule(
TEST_DOMAIN,
async_setup_entry=async_setup_entry_init,
async_unload_entry=async_unload_entry_init,
),
)
@pytest.fixture(autouse=True)
def set_time_zone(hass: HomeAssistant) -> None:
"""Set the time zone for the tests that keesp UTC-6 all year round."""
hass.config.set_time_zone("America/Regina")
2023-10-23 20:53:00 +00:00
async def create_mock_platform(
hass: HomeAssistant,
entities: list[TodoListEntity],
) -> MockConfigEntry:
"""Create a todo platform with the specified entities."""
async def async_setup_entry_platform(
hass: HomeAssistant,
config_entry: ConfigEntry,
async_add_entities: AddEntitiesCallback,
) -> None:
"""Set up test event platform via config entry."""
async_add_entities(entities)
mock_platform(
hass,
f"{TEST_DOMAIN}.{DOMAIN}",
MockPlatform(async_setup_entry=async_setup_entry_platform),
)
config_entry = MockConfigEntry(domain=TEST_DOMAIN)
config_entry.add_to_hass(hass)
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@pytest.fixture(name="test_entity")
def mock_test_entity() -> TodoListEntity:
"""Fixture that creates a test TodoList entity with mock service calls."""
entity1 = MockTodoListEntity(
[
TodoItem(summary="Item #1", uid="1", status=TodoItemStatus.NEEDS_ACTION),
TodoItem(summary="Item #2", uid="2", status=TodoItemStatus.COMPLETED),
]
)
2023-10-23 20:53:00 +00:00
entity1.entity_id = "todo.entity1"
entity1._attr_supported_features = (
TodoListEntityFeature.CREATE_TODO_ITEM
| TodoListEntityFeature.UPDATE_TODO_ITEM
| TodoListEntityFeature.DELETE_TODO_ITEM
| TodoListEntityFeature.MOVE_TODO_ITEM
)
entity1.async_create_todo_item = AsyncMock(wraps=entity1.async_create_todo_item)
2023-10-23 20:53:00 +00:00
entity1.async_update_todo_item = AsyncMock()
entity1.async_delete_todo_items = AsyncMock(wraps=entity1.async_delete_todo_items)
2023-10-23 20:53:00 +00:00
entity1.async_move_todo_item = AsyncMock()
return entity1
async def test_unload_entry(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test unloading a config entry with a todo entity."""
config_entry = await create_mock_platform(hass, [test_entity])
assert config_entry.state == ConfigEntryState.LOADED
state = hass.states.get("todo.entity1")
assert state
assert await hass.config_entries.async_unload(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state == ConfigEntryState.NOT_LOADED
state = hass.states.get("todo.entity1")
assert not state
async def test_list_todo_items(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
) -> None:
"""Test listing items in a To-do list."""
await create_mock_platform(hass, [test_entity])
state = hass.states.get("todo.entity1")
assert state
assert state.state == "1"
assert state.attributes == {"supported_features": 15}
client = await hass_ws_client(hass)
await client.send_json(
{"id": 1, "type": "todo/item/list", "entity_id": "todo.entity1"}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("success")
assert resp.get("result") == {
"items": [
ITEM_1,
ITEM_2,
2023-10-23 20:53:00 +00:00
]
}
@pytest.mark.parametrize(
("service_data", "expected_items"),
[
({}, [ITEM_1, ITEM_2]),
(
[
{"status": [TodoItemStatus.COMPLETED, TodoItemStatus.NEEDS_ACTION]},
[ITEM_1, ITEM_2],
]
),
(
[
{"status": [TodoItemStatus.NEEDS_ACTION]},
[ITEM_1],
]
),
(
[
{"status": [TodoItemStatus.COMPLETED]},
[ITEM_2],
]
),
],
)
async def test_get_items_service(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
service_data: dict[str, Any],
expected_items: list[dict[str, Any]],
) -> None:
"""Test listing items in a To-do list from a service call."""
await create_mock_platform(hass, [test_entity])
state = hass.states.get("todo.entity1")
assert state
assert state.state == "1"
assert state.attributes == {"supported_features": 15}
result = await hass.services.async_call(
DOMAIN,
"get_items",
service_data,
target={"entity_id": "todo.entity1"},
blocking=True,
return_response=True,
)
assert result == {"todo.entity1": {"items": expected_items}}
2023-10-23 20:53:00 +00:00
async def test_unsupported_websocket(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test a To-do list for an entity that does not exist."""
2023-10-23 20:53:00 +00:00
entity1 = TodoListEntity()
entity1.entity_id = "todo.entity1"
await create_mock_platform(hass, [entity1])
client = await hass_ws_client(hass)
await client.send_json(
{
"id": 1,
"type": "todo/item/list",
"entity_id": "todo.unknown",
}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("error", {}).get("code") == "not_found"
async def test_add_item_service(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test adding an item in a To-do list."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"add_item",
{"item": "New item"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_create_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item
assert item.uid is None
assert item.summary == "New item"
assert item.status == TodoItemStatus.NEEDS_ACTION
2023-10-23 20:53:00 +00:00
async def test_add_item_service_raises(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test adding an item in a To-do list that raises an error."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
test_entity.async_create_todo_item.side_effect = HomeAssistantError("Ooops")
with pytest.raises(HomeAssistantError, match="Ooops"):
await hass.services.async_call(
DOMAIN,
"add_item",
{"item": "New item"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
@pytest.mark.parametrize(
("item_data", "expected_exception", "expected_error"),
2023-10-23 20:53:00 +00:00
[
({}, vol.Invalid, "required key not provided"),
({"item": ""}, vol.Invalid, "length of value must be at least 1"),
(
{"item": "Submit forms", "description": "Submit tax forms"},
ValueError,
"does not support setting field 'description'",
),
(
{"item": "Submit forms", "due_date": "2023-11-17"},
ValueError,
"does not support setting field 'due_date'",
),
(
{
"item": "Submit forms",
"due_datetime": f"2023-11-17T17:00:00{TEST_OFFSET}",
},
ValueError,
"does not support setting field 'due_datetime'",
),
2023-10-23 20:53:00 +00:00
],
)
async def test_add_item_service_invalid_input(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
item_data: dict[str, Any],
expected_exception: str,
2023-10-23 20:53:00 +00:00
expected_error: str,
) -> None:
"""Test invalid input to the add item service."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
with pytest.raises(expected_exception, match=expected_error):
2023-10-23 20:53:00 +00:00
await hass.services.async_call(
DOMAIN,
"add_item",
2023-10-23 20:53:00 +00:00
item_data,
target={"entity_id": "todo.entity1"},
blocking=True,
)
@pytest.mark.parametrize(
("supported_entity_feature", "item_data", "expected_item"),
(
(
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
{"item": "New item", "due_date": "2023-11-13"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.date(2023, 11, 13),
),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"item": "New item", "due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.datetime(2023, 11, 13, 17, 00, 00, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"item": "New item", "due_datetime": "2023-11-13T17:00:00+00:00"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.datetime(2023, 11, 13, 11, 00, 00, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"item": "New item", "due_datetime": "2023-11-13"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
due=datetime.datetime(2023, 11, 13, 0, 00, 00, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
{"item": "New item", "description": "Submit revised draft"},
TodoItem(
summary="New item",
status=TodoItemStatus.NEEDS_ACTION,
description="Submit revised draft",
),
),
),
)
async def test_add_item_service_extended_fields(
hass: HomeAssistant,
test_entity: TodoListEntity,
supported_entity_feature: int,
item_data: dict[str, Any],
expected_item: TodoItem,
) -> None:
"""Test adding an item in a To-do list."""
test_entity._attr_supported_features |= supported_entity_feature
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"add_item",
{"item": "New item", **item_data},
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_create_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item == expected_item
2023-10-23 20:53:00 +00:00
async def test_update_todo_item_service_by_id(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", "rename": "Updated item", "status": "completed"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item
assert item.uid == "1"
2023-10-23 20:53:00 +00:00
assert item.summary == "Updated item"
assert item.status == TodoItemStatus.COMPLETED
async def test_update_todo_item_service_by_id_status_only(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", "status": "completed"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item
assert item.uid == "1"
2023-10-23 20:53:00 +00:00
assert item.summary is None
assert item.status == TodoItemStatus.COMPLETED
async def test_update_todo_item_service_by_id_rename(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", "rename": "Updated item"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item
assert item.uid == "1"
2023-10-23 20:53:00 +00:00
assert item.summary == "Updated item"
assert item.status is None
async def test_update_todo_item_service_raises(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list that raises an error."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", "rename": "Updated item", "status": "completed"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
test_entity.async_update_todo_item.side_effect = HomeAssistantError("Ooops")
with pytest.raises(HomeAssistantError, match="Ooops"):
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", "rename": "Updated item", "status": "completed"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
async def test_update_todo_item_service_by_summary(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list by summary."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "Item #1", "rename": "Something else", "status": "completed"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item
assert item.uid == "1"
assert item.summary == "Something else"
2023-10-23 20:53:00 +00:00
assert item.status == TodoItemStatus.COMPLETED
async def test_update_todo_item_service_by_summary_only_status(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list by summary."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "Item #1", "rename": "Something else"},
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item
assert item.uid == "1"
assert item.summary == "Something else"
assert item.status is None
2023-10-23 20:53:00 +00:00
async def test_update_todo_item_service_by_summary_not_found(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test updating an item in a To-do list by summary which is not found."""
await create_mock_platform(hass, [test_entity])
with pytest.raises(ValueError, match="Unable to find"):
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "Item #7", "status": "completed"},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
@pytest.mark.parametrize(
("item_data", "expected_error"),
[
({}, r"required key not provided @ data\['item'\]"),
({"status": "needs_action"}, r"required key not provided @ data\['item'\]"),
({"item": "Item #1"}, "must contain at least one of"),
2023-10-23 20:53:00 +00:00
(
{"item": "", "status": "needs_action"},
2023-10-23 20:53:00 +00:00
"length of value must be at least 1",
),
],
)
async def test_update_item_service_invalid_input(
hass: HomeAssistant,
test_entity: TodoListEntity,
item_data: dict[str, Any],
expected_error: str,
) -> None:
"""Test invalid input to the update item service."""
await create_mock_platform(hass, [test_entity])
with pytest.raises(vol.Invalid, match=expected_error):
await hass.services.async_call(
DOMAIN,
"update_item",
item_data,
target={"entity_id": "todo.entity1"},
blocking=True,
)
@pytest.mark.parametrize(
("update_data"),
[
({"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"}),
({"due_date": "2023-11-13"}),
({"description": "Submit revised draft"}),
],
)
async def test_update_todo_item_field_unsupported(
hass: HomeAssistant,
test_entity: TodoListEntity,
update_data: dict[str, Any],
) -> None:
"""Test updating an item in a To-do list."""
await create_mock_platform(hass, [test_entity])
with pytest.raises(ValueError, match="does not support"):
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", **update_data},
target={"entity_id": "todo.entity1"},
blocking=True,
)
@pytest.mark.parametrize(
("supported_entity_feature", "update_data", "expected_update"),
(
(
TodoListEntityFeature.SET_DUE_DATE_ON_ITEM,
{"due_date": "2023-11-13"},
TodoItem(uid="1", due=datetime.date(2023, 11, 13)),
),
(
TodoListEntityFeature.SET_DUE_DATETIME_ON_ITEM,
{"due_datetime": f"2023-11-13T17:00:00{TEST_OFFSET}"},
TodoItem(
uid="1",
due=datetime.datetime(2023, 11, 13, 17, 0, 0, tzinfo=TEST_TIMEZONE),
),
),
(
TodoListEntityFeature.SET_DESCRIPTION_ON_ITEM,
{"description": "Submit revised draft"},
TodoItem(uid="1", description="Submit revised draft"),
),
),
)
async def test_update_todo_item_extended_fields(
hass: HomeAssistant,
test_entity: TodoListEntity,
supported_entity_feature: int,
update_data: dict[str, Any],
expected_update: TodoItem,
) -> None:
"""Test updating an item in a To-do list."""
test_entity._attr_supported_features |= supported_entity_feature
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"update_item",
{"item": "1", **update_data},
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_update_todo_item.call_args
assert args
item = args.kwargs.get("item")
assert item == expected_update
async def test_remove_todo_item_service_by_id(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test removing an item in a To-do list."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"remove_item",
{"item": ["1", "2"]},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_delete_todo_items.call_args
assert args
assert args.kwargs.get("uids") == ["1", "2"]
2023-10-23 20:53:00 +00:00
async def test_remove_todo_item_service_raises(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test removing an item in a To-do list that raises an error."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
test_entity.async_delete_todo_items.side_effect = HomeAssistantError("Ooops")
with pytest.raises(HomeAssistantError, match="Ooops"):
await hass.services.async_call(
DOMAIN,
"remove_item",
{"item": ["1", "2"]},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
async def test_remove_todo_item_service_invalid_input(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test invalid input to the remove item service."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
with pytest.raises(
vol.Invalid, match=r"required key not provided @ data\['item'\]"
):
2023-10-23 20:53:00 +00:00
await hass.services.async_call(
DOMAIN,
"remove_item",
2023-10-23 20:53:00 +00:00
{},
target={"entity_id": "todo.entity1"},
blocking=True,
)
async def test_remove_todo_item_service_by_summary(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test removing an item in a To-do list by summary."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"remove_item",
{"item": ["Item #1"]},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_delete_todo_items.call_args
assert args
assert args.kwargs.get("uids") == ["1"]
async def test_remove_todo_item_service_by_summary_not_found(
2023-10-23 20:53:00 +00:00
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test removing an item in a To-do list by summary which is not found."""
2023-10-23 20:53:00 +00:00
await create_mock_platform(hass, [test_entity])
with pytest.raises(ValueError, match="Unable to find"):
await hass.services.async_call(
DOMAIN,
"remove_item",
{"item": ["Item #7"]},
2023-10-23 20:53:00 +00:00
target={"entity_id": "todo.entity1"},
blocking=True,
)
async def test_move_todo_item_service_by_id(
hass: HomeAssistant,
test_entity: TodoListEntity,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test moving an item in a To-do list."""
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "todo/item/move",
"entity_id": "todo.entity1",
"uid": "item-1",
"previous_uid": "item-2",
2023-10-23 20:53:00 +00:00
}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("success")
args = test_entity.async_move_todo_item.call_args
assert args
assert args.kwargs.get("uid") == "item-1"
assert args.kwargs.get("previous_uid") == "item-2"
2023-10-23 20:53:00 +00:00
async def test_move_todo_item_service_raises(
hass: HomeAssistant,
test_entity: TodoListEntity,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test moving an item in a To-do list that raises an error."""
await create_mock_platform(hass, [test_entity])
test_entity.async_move_todo_item.side_effect = HomeAssistantError("Ooops")
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "todo/item/move",
"entity_id": "todo.entity1",
"uid": "item-1",
"previous_uid": "item-2",
2023-10-23 20:53:00 +00:00
}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("error", {}).get("code") == "failed"
assert resp.get("error", {}).get("message") == "Ooops"
@pytest.mark.parametrize(
("item_data", "expected_status", "expected_error"),
[
(
{"entity_id": "todo.unknown", "uid": "item-1"},
"not_found",
"Entity not found",
),
({"entity_id": "todo.entity1"}, "invalid_format", "required key not provided"),
(
{"entity_id": "todo.entity1", "previous_uid": "item-2"},
2023-10-23 20:53:00 +00:00
"invalid_format",
"required key not provided",
),
],
)
async def test_move_todo_item_service_invalid_input(
hass: HomeAssistant,
test_entity: TodoListEntity,
hass_ws_client: WebSocketGenerator,
item_data: dict[str, Any],
expected_status: str,
expected_error: str,
) -> None:
"""Test invalid input for the move item service."""
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "todo/item/move",
**item_data,
}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("error", {}).get("code") == expected_status
assert expected_error in resp.get("error", {}).get("message")
@pytest.mark.parametrize(
("service_name", "payload"),
[
(
"add_item",
2023-10-23 20:53:00 +00:00
{
"item": "New item",
2023-10-23 20:53:00 +00:00
},
),
(
"remove_item",
2023-10-23 20:53:00 +00:00
{
"item": ["1"],
2023-10-23 20:53:00 +00:00
},
),
(
"update_item",
{
"item": "1",
"rename": "Updated item",
2023-10-23 20:53:00 +00:00
},
),
(
"remove_completed_items",
None,
),
2023-10-23 20:53:00 +00:00
],
)
async def test_unsupported_service(
hass: HomeAssistant,
service_name: str,
payload: dict[str, Any] | None,
2023-10-23 20:53:00 +00:00
) -> None:
"""Test a To-do list that does not support features."""
entity1 = TodoListEntity()
entity1.entity_id = "todo.entity1"
await create_mock_platform(hass, [entity1])
with pytest.raises(
HomeAssistantError,
match="does not support this service",
):
await hass.services.async_call(
DOMAIN,
service_name,
payload,
target={"entity_id": "todo.entity1"},
blocking=True,
)
async def test_move_item_unsupported(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test invalid input for the move item service."""
entity1 = TodoListEntity()
entity1.entity_id = "todo.entity1"
await create_mock_platform(hass, [entity1])
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "todo/item/move",
"entity_id": "todo.entity1",
"uid": "item-1",
"previous_uid": "item-2",
2023-10-23 20:53:00 +00:00
}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("error", {}).get("code") == "not_supported"
async def test_add_item_intent(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test adding items to lists using an intent."""
await todo_intent.async_setup_intents(hass)
entity1 = MockTodoListEntity()
entity1._attr_name = "List 1"
entity1.entity_id = "todo.list_1"
entity2 = MockTodoListEntity()
entity2._attr_name = "List 2"
entity2.entity_id = "todo.list_2"
await create_mock_platform(hass, [entity1, entity2])
# Add to first list
response = await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "beer"}, "name": {"value": "list 1"}},
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert len(entity1.items) == 1
assert len(entity2.items) == 0
assert entity1.items[0].summary == "beer"
assert entity1.items[0].status == TodoItemStatus.NEEDS_ACTION
entity1.items.clear()
# Add to second list
response = await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "cheese"}, "name": {"value": "List 2"}},
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert len(entity1.items) == 0
assert len(entity2.items) == 1
assert entity2.items[0].summary == "cheese"
assert entity2.items[0].status == TodoItemStatus.NEEDS_ACTION
# List name is case insensitive
response = await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": "lIST 2"}},
)
assert response.response_type == intent.IntentResponseType.ACTION_DONE
assert len(entity1.items) == 0
assert len(entity2.items) == 2
assert entity2.items[1].summary == "wine"
assert entity2.items[1].status == TodoItemStatus.NEEDS_ACTION
# Missing list
with pytest.raises(intent.IntentHandleError):
await intent.async_handle(
hass,
"test",
todo_intent.INTENT_LIST_ADD_ITEM,
{"item": {"value": "wine"}, "name": {"value": "This list does not exist"}},
)
async def test_remove_completed_items_service(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test remove completed todo items service."""
await create_mock_platform(hass, [test_entity])
await hass.services.async_call(
DOMAIN,
"remove_completed_items",
target={"entity_id": "todo.entity1"},
blocking=True,
)
args = test_entity.async_delete_todo_items.call_args
assert args
assert args.kwargs.get("uids") == ["2"]
test_entity.async_delete_todo_items.reset_mock()
# calling service multiple times will not call the entity method
await hass.services.async_call(
DOMAIN,
"remove_completed_items",
target={"entity_id": "todo.entity1"},
blocking=True,
)
test_entity.async_delete_todo_items.assert_not_called()
async def test_remove_completed_items_service_raises(
hass: HomeAssistant,
test_entity: TodoListEntity,
) -> None:
"""Test removing all completed item from a To-do list that raises an error."""
await create_mock_platform(hass, [test_entity])
test_entity.async_delete_todo_items.side_effect = HomeAssistantError("Ooops")
with pytest.raises(HomeAssistantError, match="Ooops"):
await hass.services.async_call(
DOMAIN,
"remove_completed_items",
target={"entity_id": "todo.entity1"},
blocking=True,
)
async def test_subscribe(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
) -> None:
"""Test subscribing to todo updates."""
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "todo/item/subscribe",
"entity_id": test_entity.entity_id,
}
)
msg = await client.receive_json()
assert msg["success"]
assert msg["result"] is None
subscription_id = msg["id"]
msg = await client.receive_json()
assert msg["id"] == subscription_id
assert msg["type"] == "event"
event_message = msg["event"]
assert event_message == {
"items": [
{
"summary": "Item #1",
"uid": "1",
"status": "needs_action",
"due": None,
"description": None,
},
{
"summary": "Item #2",
"uid": "2",
"status": "completed",
"due": None,
"description": None,
},
]
}
test_entity._attr_todo_items = [
*test_entity._attr_todo_items,
TodoItem(summary="Item #3", uid="3", status=TodoItemStatus.NEEDS_ACTION),
]
test_entity.async_write_ha_state()
msg = await client.receive_json()
event_message = msg["event"]
assert event_message == {
"items": [
{
"summary": "Item #1",
"uid": "1",
"status": "needs_action",
"due": None,
"description": None,
},
{
"summary": "Item #2",
"uid": "2",
"status": "completed",
"due": None,
"description": None,
},
{
"summary": "Item #3",
"uid": "3",
"status": "needs_action",
"due": None,
"description": None,
},
]
}
test_entity._attr_todo_items = None
test_entity.async_write_ha_state()
msg = await client.receive_json()
event_message = msg["event"]
assert event_message == {
"items": [],
}
async def test_subscribe_entity_does_not_exist(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
) -> None:
"""Test failure to subscribe to an entity that does not exist."""
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client(hass)
await client.send_json_auto_id(
{
"type": "todo/item/subscribe",
"entity_id": "todo.unknown",
}
)
msg = await client.receive_json()
assert not msg["success"]
assert msg["error"] == {
"code": "invalid_entity_id",
"message": "To-do list entity not found: todo.unknown",
}
@pytest.mark.parametrize(
("item_data", "expected_item_data"),
[
({"due": datetime.date(2023, 11, 17)}, {"due": "2023-11-17"}),
(
{"due": datetime.datetime(2023, 11, 17, 17, 0, 0, tzinfo=TEST_TIMEZONE)},
{"due": f"2023-11-17T17:00:00{TEST_OFFSET}"},
),
({"description": "Some description"}, {"description": "Some description"}),
],
)
async def test_list_todo_items_extended_fields(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
test_entity: TodoListEntity,
item_data: dict[str, Any],
expected_item_data: dict[str, Any],
) -> None:
"""Test listing items in a To-do list with extended fields."""
test_entity._attr_todo_items = [
TodoItem(
**ITEM_1,
**item_data,
),
]
await create_mock_platform(hass, [test_entity])
client = await hass_ws_client(hass)
await client.send_json(
{"id": 1, "type": "todo/item/list", "entity_id": "todo.entity1"}
)
resp = await client.receive_json()
assert resp.get("id") == 1
assert resp.get("success")
assert resp.get("result") == {
"items": [
{
**ITEM_1,
**expected_item_data,
},
]
}
result = await hass.services.async_call(
DOMAIN,
"get_items",
{},
target={"entity_id": "todo.entity1"},
blocking=True,
return_response=True,
)
assert result == {
"todo.entity1": {
"items": [
{
**ITEM_1,
**expected_item_data,
},
]
}
}