core/tests/components/habitica/test_todo.py

772 lines
22 KiB
Python

"""Tests for Habitica todo platform."""
from collections.abc import Generator
from datetime import date
from typing import Any
from unittest.mock import AsyncMock, patch
from uuid import UUID
from habiticalib import Direction, HabiticaTasksResponse, Task, TaskType
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.habitica.const import DOMAIN
from homeassistant.components.todo import (
ATTR_DESCRIPTION,
ATTR_DUE_DATE,
ATTR_ITEM,
ATTR_RENAME,
ATTR_STATUS,
DOMAIN as TODO_DOMAIN,
TodoServices,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er
from .conftest import ERROR_NOT_FOUND, ERROR_TOO_MANY_REQUESTS
from tests.common import (
MockConfigEntry,
async_get_persistent_notifications,
load_fixture,
snapshot_platform,
)
from tests.typing import WebSocketGenerator
@pytest.fixture(autouse=True)
def todo_only() -> Generator[None]:
"""Enable only the todo platform."""
with patch(
"homeassistant.components.habitica.PLATFORMS",
[Platform.TODO],
):
yield
@pytest.mark.usefixtures("habitica")
async def test_todos(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_registry: er.EntityRegistry,
) -> None:
"""Test todo platform."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await snapshot_platform(hass, entity_registry, snapshot, config_entry.entry_id)
@pytest.mark.parametrize(
("entity_id"),
[
"todo.test_user_to_do_s",
"todo.test_user_dailies",
],
)
@pytest.mark.usefixtures("habitica")
async def test_todo_items(
hass: HomeAssistant,
config_entry: MockConfigEntry,
snapshot: SnapshotAssertion,
entity_id: str,
) -> None:
"""Test items on todo lists."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
result = await hass.services.async_call(
TODO_DOMAIN,
TodoServices.GET_ITEMS,
{},
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
return_response=True,
)
assert result == snapshot
@pytest.mark.freeze_time("2024-09-21 00:00:00")
@pytest.mark.parametrize(
("entity_id", "uid"),
[
("todo.test_user_to_do_s", "88de7cd9-af2b-49ce-9afd-bf941d87336b"),
("todo.test_user_dailies", "f2c85972-1a19-4426-bc6d-ce3337b9d99f"),
],
ids=["todo", "daily"],
)
async def test_complete_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
snapshot: SnapshotAssertion,
entity_id: str,
uid: str,
) -> None:
"""Test completing an item on the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{ATTR_ITEM: uid, ATTR_STATUS: "completed"},
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
habitica.update_score.assert_awaited_once_with(UUID(uid), Direction.UP)
# Test notification for item drop
notifications = async_get_persistent_notifications(hass)
assert len(notifications) == 1
_id, *_ = notifications
assert snapshot == (notifications[_id]["title"], notifications[_id]["message"])
@pytest.mark.parametrize(
("entity_id", "uid"),
[
("todo.test_user_to_do_s", "162f0bbe-a097-4a06-b4f4-8fbeed85d2ba"),
("todo.test_user_dailies", "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"),
],
ids=["todo", "daily"],
)
async def test_uncomplete_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
entity_id: str,
uid: str,
) -> None:
"""Test uncompleting an item on the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{ATTR_ITEM: uid, ATTR_STATUS: "needs_action"},
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
habitica.update_score.assert_called_once_with(UUID(uid), Direction.DOWN)
@pytest.mark.parametrize(
("uid", "status"),
[
("88de7cd9-af2b-49ce-9afd-bf941d87336b", "completed"),
("162f0bbe-a097-4a06-b4f4-8fbeed85d2ba", "needs_action"),
],
ids=["completed", "needs_action"],
)
@pytest.mark.parametrize(
("exception", "exc_msg", "expected_exception"),
[
(
ERROR_NOT_FOUND,
r"Unable to update the score for your Habitica to-do `.+`, please try again",
ServiceValidationError,
),
(
ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again in 5 seconds",
HomeAssistantError,
),
],
)
async def test_complete_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
uid: str,
status: str,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None:
"""Test exception when completing/uncompleting an item on the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
habitica.update_score.side_effect = exception
with pytest.raises(
expected_exception=expected_exception,
match=exc_msg,
):
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{ATTR_ITEM: uid, ATTR_STATUS: status},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
@pytest.mark.parametrize(
("entity_id", "service_data", "call_args"),
[
(
"todo.test_user_to_do_s",
{
ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
ATTR_RENAME: "test-summary",
ATTR_DESCRIPTION: "test-description",
ATTR_DUE_DATE: date(2024, 7, 30),
},
(
UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
Task(
notes="test-description",
text="test-summary",
date=date(2024, 7, 30),
),
),
),
(
"todo.test_user_to_do_s",
{
ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
ATTR_RENAME: "test-summary",
ATTR_DESCRIPTION: "test-description",
ATTR_DUE_DATE: None,
},
(
UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
Task(
notes="test-description",
text="test-summary",
date=None,
),
),
),
(
"todo.test_user_to_do_s",
{
ATTR_ITEM: "88de7cd9-af2b-49ce-9afd-bf941d87336b",
ATTR_RENAME: "test-summary",
ATTR_DESCRIPTION: None,
ATTR_DUE_DATE: date(2024, 7, 30),
},
(
UUID("88de7cd9-af2b-49ce-9afd-bf941d87336b"),
Task(
notes="",
text="test-summary",
date=date(2024, 7, 30),
),
),
),
(
"todo.test_user_dailies",
{
ATTR_ITEM: "f2c85972-1a19-4426-bc6d-ce3337b9d99f",
ATTR_RENAME: "test-summary",
ATTR_DESCRIPTION: "test-description",
},
(
UUID("f2c85972-1a19-4426-bc6d-ce3337b9d99f"),
Task(
notes="test-description",
text="test-summary",
),
),
),
],
ids=["todo", "todo remove date", "todo remove notes", "daily"],
)
async def test_update_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
entity_id: str,
service_data: dict[str, Any],
call_args: tuple[UUID, Task],
) -> None:
"""Test update details of an item on the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
service_data,
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
habitica.update_task.assert_awaited_once_with(*call_args)
@pytest.mark.parametrize(
("exception", "exc_msg", "expected_exception"),
[
(
ERROR_NOT_FOUND,
"Unable to update the Habitica to-do `test-summary`, please try again",
ServiceValidationError,
),
(
ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again in 5 seconds",
HomeAssistantError,
),
],
)
async def test_update_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None:
"""Test exception when update item on the todo list."""
uid = "88de7cd9-af2b-49ce-9afd-bf941d87336b"
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
habitica.update_task.side_effect = exception
with pytest.raises(expected_exception=expected_exception, match=exc_msg):
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{
ATTR_ITEM: uid,
ATTR_RENAME: "test-summary",
ATTR_DESCRIPTION: "test-description",
ATTR_DUE_DATE: "2024-07-30",
},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
async def test_add_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
) -> None:
"""Test add a todo item to the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.ADD_ITEM,
{
ATTR_ITEM: "test-summary",
ATTR_DESCRIPTION: "test-description",
ATTR_DUE_DATE: "2024-07-30",
},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
habitica.create_task.assert_awaited_once_with(
Task(
date=date(2024, 7, 30),
notes="test-description",
text="test-summary",
type=TaskType.TODO,
)
)
@pytest.mark.parametrize(
("exception", "exc_msg", "expected_exception"),
[
(
ERROR_NOT_FOUND,
"Unable to create new to-do `test-summary` for Habitica, please try again",
ServiceValidationError,
),
(
ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again in 5 seconds",
HomeAssistantError,
),
],
)
async def test_add_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None:
"""Test exception when adding a todo item to the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
habitica.create_task.side_effect = exception
with pytest.raises(
expected_exception=expected_exception,
# match="Unable to create new to-do `test-summary` for Habitica, please try again",
match=exc_msg,
):
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.ADD_ITEM,
{
ATTR_ITEM: "test-summary",
ATTR_DESCRIPTION: "test-description",
ATTR_DUE_DATE: "2024-07-30",
},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
async def test_delete_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
) -> None:
"""Test deleting a todo item from the todo list."""
uid = "2f6fcabc-f670-4ec3-ba65-817e8deea490"
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.REMOVE_ITEM,
{ATTR_ITEM: uid},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
habitica.delete_task.assert_awaited_once_with(UUID(uid))
@pytest.mark.parametrize(
("exception", "exc_msg", "expected_exception"),
[
(
ERROR_NOT_FOUND,
"Unable to delete item from Habitica to-do list, please try again",
ServiceValidationError,
),
(
ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again in 5 seconds",
HomeAssistantError,
),
],
)
async def test_delete_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None:
"""Test exception when deleting a todo item from the todo list."""
uid = "2f6fcabc-f670-4ec3-ba65-817e8deea490"
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
habitica.delete_task.side_effect = exception
with pytest.raises(
expected_exception=expected_exception,
match=exc_msg,
):
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.REMOVE_ITEM,
{ATTR_ITEM: uid},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
async def test_delete_completed_todo_items(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
) -> None:
"""Test deleting completed todo items from the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.REMOVE_COMPLETED_ITEMS,
{},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
habitica.delete_completed_todos.assert_awaited_once()
@pytest.mark.parametrize(
("exception", "exc_msg", "expected_exception"),
[
(
ERROR_NOT_FOUND,
"Unable to delete completed to-do items from Habitica to-do list, please try again",
ServiceValidationError,
),
(
ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again in 5 seconds",
HomeAssistantError,
),
],
)
async def test_delete_completed_todo_items_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None:
"""Test exception when deleting completed todo items from the todo list."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
habitica.delete_completed_todos.side_effect = exception
with pytest.raises(
expected_exception=expected_exception,
match=exc_msg,
):
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.REMOVE_COMPLETED_ITEMS,
{},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
@pytest.mark.parametrize(
("entity_id", "uid", "previous_uid"),
[
(
"todo.test_user_to_do_s",
"1aa3137e-ef72-4d1f-91ee-41933602f438",
"88de7cd9-af2b-49ce-9afd-bf941d87336b",
),
(
"todo.test_user_dailies",
"2c6d136c-a1c3-4bef-b7c4-fa980784b1e1",
"564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
),
],
ids=["todo", "daily"],
)
async def test_move_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
hass_ws_client: WebSocketGenerator,
entity_id: str,
uid: str,
previous_uid: str,
) -> None:
"""Test move todo items."""
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
client = await hass_ws_client()
# move to second position
data = {
"id": id,
"type": "todo/item/move",
"entity_id": entity_id,
"uid": uid,
"previous_uid": previous_uid,
}
await client.send_json_auto_id(data)
resp = await client.receive_json()
assert resp.get("success")
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 1)
habitica.reorder_task.reset_mock()
# move to top position
data = {
"id": id,
"type": "todo/item/move",
"entity_id": entity_id,
"uid": uid,
}
await client.send_json_auto_id(data)
resp = await client.receive_json()
assert resp.get("success")
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)
@pytest.mark.parametrize(
("exception", "exc_msg"),
[
(
ERROR_NOT_FOUND,
"Unable to move the Habitica to-do to position 0, please try again",
),
(
ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again in 5 seconds",
),
],
)
async def test_move_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
habitica: AsyncMock,
hass_ws_client: WebSocketGenerator,
exception: Exception,
exc_msg: str,
) -> None:
"""Test exception when moving todo item."""
uid = "1aa3137e-ef72-4d1f-91ee-41933602f438"
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
habitica.reorder_task.side_effect = exception
client = await hass_ws_client()
data = {
"id": id,
"type": "todo/item/move",
"entity_id": "todo.test_user_to_do_s",
"uid": uid,
}
await client.send_json_auto_id(data)
resp = await client.receive_json()
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)
assert resp["success"] is False
assert resp["error"]["message"] == exc_msg
@pytest.mark.parametrize(
("fixture", "calculated_due_date"),
[
("duedate_fixture_1.json", "2024-09-22"),
("duedate_fixture_2.json", "2024-09-24"),
("duedate_fixture_3.json", "2024-10-23"),
("duedate_fixture_4.json", "2024-10-23"),
("duedate_fixture_5.json", "2024-09-28"),
("duedate_fixture_6.json", "2024-10-21"),
("duedate_fixture_7.json", None),
("duedate_fixture_8.json", None),
("duedate_fixture_9.json", None),
],
ids=[
"default",
"daily starts on startdate",
"monthly starts on startdate",
"yearly starts on startdate",
"weekly",
"monthly starts on fixed day",
"grey daily",
"empty nextDue",
"grey daily no weekdays",
],
)
@pytest.mark.usefixtures("set_tz")
async def test_next_due_date(
hass: HomeAssistant,
fixture: str,
calculated_due_date: str | None,
config_entry: MockConfigEntry,
habitica: AsyncMock,
) -> None:
"""Test next_due_date calculation."""
dailies_entity = "todo.test_user_dailies"
habitica.get_tasks.side_effect = [
HabiticaTasksResponse.from_json(load_fixture(fixture, DOMAIN)),
HabiticaTasksResponse.from_dict({"success": True, "data": []}),
]
config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
result = await hass.services.async_call(
TODO_DOMAIN,
TodoServices.GET_ITEMS,
{},
target={ATTR_ENTITY_ID: dailies_entity},
blocking=True,
return_response=True,
)
assert result[dailies_entity]["items"][0].get("due") == calculated_due_date