1
0
mirror of https://github.com/home-assistant/core.git synced 2026-07-08 15:24:58 +01:00
Files
Léon 980d3c28fd Add To-do list platform to Alexa Devices (#171136)
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Simone Chemelli <simone.chemelli@gmail.com>
2026-06-22 18:22:41 +02:00

461 lines
14 KiB
Python

"""Test Alexa Devices todo entities."""
from typing import Any
from unittest.mock import AsyncMock, patch
from aioamazondevices import CannotAuthenticate, CannotConnect
from aioamazondevices.exceptions import CannotRetrieveData
from aioamazondevices.structures import (
AmazonListEvent,
AmazonListEventType,
AmazonListInfo,
AmazonListItem,
AmazonListItemStatus,
AmazonListType,
)
from freezegun.api import FrozenDateTimeFactory
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
from homeassistant.components.todo import (
DOMAIN as TODO_DOMAIN,
TodoItemStatus,
TodoServices,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.util import slugify
from . import setup_integration
from .const import TEST_USERNAME
from tests.common import MockConfigEntry, async_fire_time_changed, snapshot_platform
LIST_ENTITY_ID_PREFIX = f"{TODO_DOMAIN.lower()}.{slugify(TEST_USERNAME)}_"
MOCK_SHOPPING_LIST_ENTITY_ID = f"{LIST_ENTITY_ID_PREFIX}shopping_list"
MOCK_TODO_LIST_ENTITY_ID = f"{LIST_ENTITY_ID_PREFIX}to_do_list"
MOCK_CUSTOM_LIST_ENTITY_ID = f"{LIST_ENTITY_ID_PREFIX}concerts"
MOCK_SHOPPING_LIST = AmazonListInfo(
id="shopping_list_id", name=None, list_type=AmazonListType.SHOP
)
MOCK_TODO_LIST = AmazonListInfo(
id="todo_list_id", name=None, list_type=AmazonListType.TODO
)
MOCK_CUSTOM_LIST = AmazonListInfo(
id="custom_list_id", name="Concerts", list_type=AmazonListType.CUSTOM
)
@pytest.fixture
def mock_todo_lists():
"""Mock todo lists."""
return [
MOCK_SHOPPING_LIST,
MOCK_TODO_LIST,
MOCK_CUSTOM_LIST,
]
@pytest.fixture
def mock_todo_items():
"""Mock todo items."""
return {
"shopping_list_id": {
"item_1": AmazonListItem(
id="item_1",
name="Bubble tea",
status=AmazonListItemStatus.ACTIVE,
version=1,
),
},
"todo_list_id": {
"item_2": AmazonListItem(
id="item_2",
name="Task 1",
status=AmazonListItemStatus.ACTIVE,
version=1,
),
"item_3": AmazonListItem(
id="item_3",
name="Task 2",
status=AmazonListItemStatus.COMPLETE,
version=2,
),
},
"custom_list_id": {
"item_4": AmazonListItem(
id="item_4",
name="TWICE",
status=AmazonListItemStatus.ACTIVE,
version=1,
),
"item_5": AmazonListItem(
id="item_5",
name="BTS",
status=AmazonListItemStatus.COMPLETE,
version=2,
),
},
}
async def test_all_entities(
hass: HomeAssistant,
snapshot: SnapshotAssertion,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
entity_registry: er.EntityRegistry,
mock_todo_lists: list[AmazonListInfo],
mock_todo_items: dict[str, Any],
) -> None:
"""Test all entities."""
mock_amazon_devices_client.todo_lists = mock_todo_lists
mock_amazon_devices_client.get_todo_list_items = AsyncMock(
side_effect=lambda list_id: mock_todo_items.get(list_id, {})
)
with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.TODO]):
await setup_integration(hass, mock_config_entry)
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
async def test_add_todo_item(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_todo_lists: list[AmazonListInfo],
) -> None:
"""Test adding a todo item."""
mock_amazon_devices_client.todo_lists = mock_todo_lists
mock_amazon_devices_client.get_todo_list_items = AsyncMock(return_value={})
await setup_integration(hass, mock_config_entry)
entity_id = MOCK_TODO_LIST_ENTITY_ID
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.ADD_ITEM,
{ATTR_ENTITY_ID: entity_id, "item": "New Task"},
blocking=True,
)
mock_amazon_devices_client.add_todo_list_item.assert_called_once_with(
"todo_list_id", "New Task"
)
async def test_delete_todo_item(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_todo_lists: list[AmazonListInfo],
mock_todo_items: dict[str, Any],
) -> None:
"""Test deleting a todo item."""
mock_amazon_devices_client.todo_lists = mock_todo_lists
mock_amazon_devices_client.get_todo_list_items = AsyncMock(
side_effect=lambda list_id: mock_todo_items.get(list_id, {})
)
await setup_integration(hass, mock_config_entry)
entity_id = MOCK_TODO_LIST_ENTITY_ID
# Delete item_2
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.REMOVE_ITEM,
{ATTR_ENTITY_ID: entity_id, "item": ["item_2"]},
blocking=True,
)
mock_amazon_devices_client.delete_todo_list_item.assert_called_once_with(
"todo_list_id", "item_2", 1
)
async def test_update_todo_item(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_todo_lists: list[AmazonListInfo],
mock_todo_items: dict[str, Any],
) -> None:
"""Test updating a todo item."""
mock_amazon_devices_client.todo_lists = mock_todo_lists
mock_amazon_devices_client.get_todo_list_items = AsyncMock(
side_effect=lambda list_id: mock_todo_items.get(list_id, {})
)
await setup_integration(hass, mock_config_entry)
entity_id = MOCK_TODO_LIST_ENTITY_ID
# Update item_2 (ACTIVE -> COMPLETE)
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{
ATTR_ENTITY_ID: entity_id,
"item": "item_2",
"status": TodoItemStatus.COMPLETED,
},
blocking=True,
)
mock_amazon_devices_client.set_todo_list_item_checked_status.assert_called_once_with(
"todo_list_id", "item_2", True, 1
)
mock_amazon_devices_client.rename_todo_list_item.assert_not_called()
# Rename item_2
mock_amazon_devices_client.set_todo_list_item_checked_status.reset_mock()
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{
ATTR_ENTITY_ID: entity_id,
"item": "item_2",
"rename": "Renamed Task",
},
blocking=True,
)
mock_amazon_devices_client.rename_todo_list_item.assert_called_once_with(
"todo_list_id", "item_2", "Renamed Task", 1
)
mock_amazon_devices_client.set_todo_list_item_checked_status.assert_not_called()
# Both status and rename changed
mock_amazon_devices_client.set_todo_list_item_checked_status.reset_mock()
mock_amazon_devices_client.rename_todo_list_item.reset_mock()
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{
ATTR_ENTITY_ID: entity_id,
"item": "item_2",
"rename": "Both Changed",
"status": TodoItemStatus.COMPLETED,
},
blocking=True,
)
mock_amazon_devices_client.set_todo_list_item_checked_status.assert_called_once_with(
"todo_list_id", "item_2", True, 1
)
mock_amazon_devices_client.rename_todo_list_item.assert_called_once_with(
"todo_list_id", "item_2", "Both Changed", 2
)
@pytest.mark.parametrize(
("initial_lists", "updated_lists"),
[
([MOCK_TODO_LIST], [MOCK_TODO_LIST, MOCK_CUSTOM_LIST]), # Add a list
([MOCK_TODO_LIST, MOCK_CUSTOM_LIST], [MOCK_TODO_LIST]), # Remove a list
(
[MOCK_TODO_LIST, MOCK_SHOPPING_LIST, MOCK_CUSTOM_LIST],
[],
), # Remove all lists
],
)
async def test_dynamic_entities(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
initial_lists: list[AmazonListInfo],
updated_lists: list[AmazonListInfo],
) -> None:
"""Test entities are dynamically created and deleted."""
def get_entity_id(alexa_list: AmazonListInfo) -> str:
if alexa_list.list_type == AmazonListType.SHOP:
return MOCK_SHOPPING_LIST_ENTITY_ID
if alexa_list.list_type == AmazonListType.TODO:
return MOCK_TODO_LIST_ENTITY_ID
return MOCK_CUSTOM_LIST_ENTITY_ID
# Start with the initial set of lists from the Amazon client.
mock_amazon_devices_client.todo_lists = list(initial_lists)
mock_amazon_devices_client.get_todo_list_items = AsyncMock(return_value={})
await setup_integration(hass, mock_config_entry)
initial_entity_ids = [get_entity_id(alexa_list) for alexa_list in initial_lists]
updated_entity_ids = [get_entity_id(alexa_list) for alexa_list in updated_lists]
# Confirm the initially expected entities exist.
for entity_id in initial_entity_ids:
assert hass.states.get(entity_id) is not None
# Update the Amazon client to return the new list set.
mock_amazon_devices_client.todo_lists = list(updated_lists)
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Confirm the entities in the updated set exist.
for entity_id in updated_entity_ids:
assert hass.states.get(entity_id) is not None
# Confirm removed entities are no longer present.
for entity_id in set(initial_entity_ids) - set(updated_entity_ids):
assert hass.states.get(entity_id) is None
async def test_dynamic_add_list_and_add_item(
hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test adding a new list and then adding an item to it."""
mock_amazon_devices_client.todo_lists = []
mock_amazon_devices_client.get_todo_list_items = AsyncMock(return_value={})
await setup_integration(hass, mock_config_entry)
assert hass.states.get(MOCK_TODO_LIST_ENTITY_ID) is None
mock_amazon_devices_client.todo_lists = [MOCK_TODO_LIST]
freezer.tick(SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
# Send CREATED event for new item in the newly created list (imitate Amazon server)
new_item_id = "item_1"
new_item = AmazonListItem(
id=new_item_id,
name="New Task",
status=AmazonListItemStatus.ACTIVE,
version=1,
)
created_event = AmazonListEvent(
list_id=MOCK_TODO_LIST.id,
item_id=new_item_id,
type=AmazonListEventType.CREATED,
items=new_item,
)
coordinator = mock_config_entry.runtime_data
await coordinator.todo_event_handler(created_event)
assert hass.states.get(MOCK_TODO_LIST_ENTITY_ID) is not None
async def test_todo_event_handler(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
mock_todo_lists: list[AmazonListInfo],
) -> None:
"""Test todo event handler."""
mock_amazon_devices_client.todo_lists = mock_todo_lists
mock_amazon_devices_client.get_todo_list_items = AsyncMock(return_value={})
await setup_integration(hass, mock_config_entry)
coordinator = mock_config_entry.runtime_data
# Seed _todo_list_items
list_id = "todo_list_id"
item_id = "item_1"
item = AmazonListItem(
id=item_id,
name="Original Task",
status=AmazonListItemStatus.ACTIVE,
version=1,
)
coordinator.todo_list_items[list_id] = {item_id: item}
# Test CREATED
new_item_id = "item_2"
new_item = AmazonListItem(
id=new_item_id,
name="New Task",
status=AmazonListItemStatus.ACTIVE,
version=1,
)
created_event = AmazonListEvent(
list_id=list_id,
item_id=new_item_id,
type=AmazonListEventType.CREATED,
items=new_item,
)
await coordinator.todo_event_handler(created_event)
assert coordinator.todo_list_items[list_id][new_item_id] == new_item
# Test UPDATED
updated_item = AmazonListItem(
id=item_id,
name="Updated Task",
status=AmazonListItemStatus.COMPLETE,
version=2,
)
updated_event = AmazonListEvent(
list_id=list_id,
item_id=item_id,
type=AmazonListEventType.UPDATED,
items=updated_item,
)
await coordinator.todo_event_handler(updated_event)
assert coordinator.todo_list_items[list_id][item_id] == updated_item
# Test DELETED
deleted_event = AmazonListEvent(
list_id=list_id,
item_id=item_id,
type=AmazonListEventType.DELETED,
items=None,
)
await coordinator.todo_event_handler(deleted_event)
assert item_id not in coordinator.todo_list_items[list_id]
@pytest.mark.parametrize(
("side_effect", "expected_state"),
[
pytest.param(
CannotAuthenticate,
ConfigEntryState.SETUP_ERROR,
id="cannot_authenticate",
),
pytest.param(
CannotConnect,
ConfigEntryState.SETUP_RETRY,
id="cannot_connect",
),
pytest.param(
CannotRetrieveData,
ConfigEntryState.SETUP_RETRY,
id="cannot_retrieve_data",
),
],
)
async def test_sync_todo_list_items_error(
hass: HomeAssistant,
mock_amazon_devices_client: AsyncMock,
mock_config_entry: MockConfigEntry,
side_effect: type[Exception],
expected_state: ConfigEntryState,
) -> None:
"""Test setup fails when syncing todo list items raises an error."""
mock_amazon_devices_client.get_todo_list_items.side_effect = side_effect
mock_amazon_devices_client.todo_lists = [
AmazonListInfo(id="shopping_list_id", name=None, list_type=AmazonListType.SHOP)
]
mock_config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(mock_config_entry.entry_id)
await hass.async_block_till_done()
assert mock_config_entry.state is expected_state