Add tests for todo platform of Habitica integration (#128199)

* Add tests for todo platform

* refactor mock_called_with

* update tests
This commit is contained in:
Manu 2024-10-26 19:48:07 +02:00 committed by GitHub
parent 7d29bff136
commit fdded9e7ee
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 1386 additions and 0 deletions

View File

@ -3,9 +3,11 @@
from unittest.mock import patch
import pytest
from yarl import URL
from homeassistant.components.habitica.const import CONF_API_USER, DEFAULT_URL, DOMAIN
from homeassistant.const import CONF_API_KEY, CONF_URL
from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_json_object_fixture
from tests.test_util.aiohttp import AiohttpClientMocker
@ -21,6 +23,23 @@ def disable_plumbum():
yield
def mock_called_with(
mock_client: AiohttpClientMocker,
method: str,
url: str,
) -> tuple | None:
"""Assert request mock was called with json data."""
return next(
(
call
for call in mock_client.mock_calls
if call[0] == method.upper() and call[1] == URL(url)
),
None,
)
@pytest.fixture
def mock_habitica(aioclient_mock: AiohttpClientMocker) -> AiohttpClientMocker:
"""Mock aiohttp requests."""
@ -54,3 +73,9 @@ def mock_config_entry() -> MockConfigEntry:
},
unique_id="00000000-0000-0000-0000-000000000000",
)
@pytest.fixture
async def set_tz(hass: HomeAssistant) -> None:
"""Fixture to set timezone."""
await hass.config.async_set_time_zone("Europe/Berlin")

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "daily",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-09-22T22:00:00.000Z", "2024-09-23T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-07-06T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": true,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "daily",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-09-22T22:00:00.000Z", "2024-09-23T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-09-23T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "monthly",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-10-22T22:00:00.000Z", "2024-11-22T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-10-22T22:00:00.000Z",
"daysOfMonth": [23],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "yearly",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-10-22T22:00:00.000Z", "2025-10-22T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-10-22T22:00:00.000Z",
"daysOfMonth": [22],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "weekly",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-09-20T22:00:00.000Z", "2024-09-27T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-09-25T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "monthly",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-09-20T22:00:00.000Z", "2024-10-20T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-09-25T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "monthly",
"everyX": 0,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": ["2024-09-22T22:00:00.000Z", "2024-09-23T22:00:00.000Z"],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-09-23T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,51 @@
{
"success": true,
"data": [
{
"_id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa",
"frequency": "daily",
"everyX": 1,
"repeat": {
"m": true,
"t": true,
"w": true,
"th": true,
"f": true,
"s": true,
"su": true
},
"streak": 1,
"nextDue": [],
"yesterDaily": true,
"history": [],
"completed": false,
"collapseChecklist": false,
"type": "daily",
"text": "Zahnseide benutzen",
"notes": "Klicke um Änderungen zu machen!",
"tags": [],
"value": -2.9663035443712333,
"priority": 1,
"attribute": "str",
"challenge": {},
"group": {
"completedBy": {},
"assignedUsers": []
},
"byHabitica": false,
"startDate": "2024-09-23T22:00:00.000Z",
"daysOfMonth": [],
"weeksOfMonth": [],
"checklist": [],
"reminders": [],
"createdAt": "2024-07-07T17:51:53.268Z",
"updatedAt": "2024-09-21T22:24:20.154Z",
"userId": "5f359083-ef78-4af0-985a-0b2c6d05797c",
"isDue": false,
"id": "564b9ac9-c53d-4638-9e7f-1cd96fe19baa"
}
],
"notifications": [],
"userV": 589,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,69 @@
{
"success": true,
"data": {
"delta": 0.9999999781878414,
"_tmp": {
"quest": {
"progressDelta": 1.049999977097233
},
"drop": {
"value": 3,
"key": "Dragon",
"type": "Egg",
"dialog": "You've found a Dragon Egg!"
}
},
"buffs": {
"str": 0,
"int": 0,
"per": 0,
"con": 0,
"stealth": 0,
"streaks": false,
"seafoam": false,
"shinySeed": false,
"snowball": false,
"spookySparkles": false
},
"training": {
"int": 0,
"per": 0,
"str": 0,
"con": 0
},
"hp": 25.100000000000016,
"mp": 24,
"exp": 196,
"gp": 30.453660284128997,
"lvl": 20,
"class": "warrior",
"points": 2,
"str": 0,
"con": 0,
"int": 0,
"per": 0
},
"notifications": [
{
"type": "ITEM_RECEIVED",
"data": {
"icon": "notif_orca_mount",
"title": "Orcas for Summer Splash!",
"text": "To celebrate Summer Splash, we've given you an Orca Mount!",
"destination": "stable"
},
"seen": true,
"id": "b7a85df1-06ed-4ab1-b56d-43418fc6a5e5"
},
{
"type": "UNALLOCATED_STATS_POINTS",
"data": {
"points": 2
},
"seen": true,
"id": "bc3f8a69-231f-4eb1-ba48-a00b6c0e0f37"
}
],
"userV": 623,
"appVersion": "5.28.6"
}

View File

@ -0,0 +1,189 @@
# serializer version: 1
# name: test_complete_todo_item[daily]
tuple(
'Habitica',
'''
![Dragon](https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_Egg_Dragon.png)
You've found a Dragon Egg!
''',
)
# ---
# name: test_complete_todo_item[todo]
tuple(
'Habitica',
'''
![Dragon](https://habitica-assets.s3.amazonaws.com/mobileApp/images/Pet_Egg_Dragon.png)
You've found a Dragon Egg!
''',
)
# ---
# name: test_todo_items[todo.test_user_dailies]
dict({
'todo.test_user_dailies': dict({
'items': list([
dict({
'description': 'Klicke um Änderungen zu machen!',
'due': '2024-09-22',
'status': 'completed',
'summary': 'Zahnseide benutzen',
'uid': '564b9ac9-c53d-4638-9e7f-1cd96fe19baa',
}),
dict({
'description': 'Klicke um Deinen Terminplan festzulegen!',
'due': '2024-09-21',
'status': 'needs_action',
'summary': '5 Minuten ruhig durchatmen',
'uid': 'f2c85972-1a19-4426-bc6d-ce3337b9d99f',
}),
dict({
'description': 'Ein einstündiges Workout im Fitnessstudio absolvieren.',
'due': '2024-09-21',
'status': 'needs_action',
'summary': 'Fitnessstudio besuchen',
'uid': '2c6d136c-a1c3-4bef-b7c4-fa980784b1e1',
}),
]),
}),
})
# ---
# name: test_todo_items[todo.test_user_to_do_s]
dict({
'todo.test_user_to_do_s': dict({
'items': list([
dict({
'description': 'Das Buch, das du angefangen hast, bis zum Wochenende fertig lesen.',
'due': '2024-09-27',
'status': 'needs_action',
'summary': 'Buch zu Ende lesen',
'uid': '88de7cd9-af2b-49ce-9afd-bf941d87336b',
}),
dict({
'description': 'Strom- und Internetrechnungen rechtzeitig überweisen.',
'due': '2024-08-31',
'status': 'needs_action',
'summary': 'Rechnungen bezahlen',
'uid': '2f6fcabc-f670-4ec3-ba65-817e8deea490',
}),
dict({
'description': 'Rasen mähen und die Pflanzen gießen.',
'status': 'needs_action',
'summary': 'Garten pflegen',
'uid': '1aa3137e-ef72-4d1f-91ee-41933602f438',
}),
dict({
'description': 'Den Ausflug für das kommende Wochenende organisieren.',
'due': '2024-09-26',
'status': 'needs_action',
'summary': 'Wochenendausflug planen',
'uid': '86ea2475-d1b5-4020-bdcc-c188c7996afa',
}),
dict({
'description': 'Lebensmittel und Haushaltsbedarf für die Woche einkaufen.',
'status': 'completed',
'summary': 'Wocheneinkauf erledigen',
'uid': '162f0bbe-a097-4a06-b4f4-8fbeed85d2ba',
}),
dict({
'description': 'Wohnzimmer und Küche gründlich aufräumen.',
'status': 'completed',
'summary': 'Wohnung aufräumen',
'uid': '3fa06743-aa0f-472b-af1a-f27c755e329c',
}),
]),
}),
})
# ---
# name: test_todos[todo.test_user_dailies-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'todo',
'entity_category': None,
'entity_id': 'todo.test_user_dailies',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': 'Dailies',
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': <TodoListEntityFeature: 92>,
'translation_key': <HabiticaTodoList.DAILIES: 'dailys'>,
'unique_id': '00000000-0000-0000-0000-000000000000_dailys',
'unit_of_measurement': None,
})
# ---
# name: test_todos[todo.test_user_dailies-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': 'test-user Dailies',
'supported_features': <TodoListEntityFeature: 92>,
}),
'context': <ANY>,
'entity_id': 'todo.test_user_dailies',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '2',
})
# ---
# name: test_todos[todo.test_user_to_do_s-entry]
EntityRegistryEntrySnapshot({
'aliases': set({
}),
'area_id': None,
'capabilities': None,
'config_entry_id': <ANY>,
'device_class': None,
'device_id': <ANY>,
'disabled_by': None,
'domain': 'todo',
'entity_category': None,
'entity_id': 'todo.test_user_to_do_s',
'has_entity_name': True,
'hidden_by': None,
'icon': None,
'id': <ANY>,
'labels': set({
}),
'name': None,
'options': dict({
}),
'original_device_class': None,
'original_icon': None,
'original_name': "To-Do's",
'platform': 'habitica',
'previous_unique_id': None,
'supported_features': <TodoListEntityFeature: 95>,
'translation_key': <HabiticaTodoList.TODOS: 'todos'>,
'unique_id': '00000000-0000-0000-0000-000000000000_todos',
'unit_of_measurement': None,
})
# ---
# name: test_todos[todo.test_user_to_do_s-state]
StateSnapshot({
'attributes': ReadOnlyDict({
'friendly_name': "test-user To-Do's",
'supported_features': <TodoListEntityFeature: 95>,
}),
'context': <ANY>,
'entity_id': 'todo.test_user_to_do_s',
'last_changed': <ANY>,
'last_reported': <ANY>,
'last_updated': <ANY>,
'state': '4',
})
# ---

View File

@ -0,0 +1,695 @@
"""Tests for Habitica todo platform."""
from collections.abc import Generator
from datetime import datetime
from http import HTTPStatus
import json
import re
from unittest.mock import patch
import pytest
from syrupy.assertion import SnapshotAssertion
from homeassistant.components.habitica.const import DEFAULT_URL, 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 ServiceValidationError
from homeassistant.helpers import entity_registry as er
from .conftest import mock_called_with
from tests.common import (
MockConfigEntry,
async_get_persistent_notifications,
load_json_object_fixture,
snapshot_platform,
)
from tests.test_util.aiohttp import AiohttpClientMocker
from tests.typing import WebSocketGenerator
@pytest.fixture(autouse=True)
def switch_only() -> Generator[None]:
"""Enable only the todo platform."""
with patch(
"homeassistant.components.habitica.PLATFORMS",
[Platform.TODO],
):
yield
@pytest.mark.usefixtures("mock_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("mock_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,
mock_habitica: AiohttpClientMocker,
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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/up",
json=load_json_object_fixture("score_with_drop.json", DOMAIN),
)
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{ATTR_ITEM: uid, ATTR_STATUS: "completed"},
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
assert mock_called_with(
mock_habitica, "post", f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/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,
mock_habitica: AiohttpClientMocker,
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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/down",
json={"data": {}, "success": True},
)
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,
)
assert mock_called_with(
mock_habitica, "post", f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/down"
)
@pytest.mark.parametrize(
("uid", "status"),
[
("88de7cd9-af2b-49ce-9afd-bf941d87336b", "completed"),
("162f0bbe-a097-4a06-b4f4-8fbeed85d2ba", "needs_action"),
],
ids=["completed", "needs_action"],
)
async def test_complete_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
uid: str,
status: str,
) -> 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
mock_habitica.post(
re.compile(f"{DEFAULT_URL}/api/v3/tasks/{uid}/score/.+"),
status=HTTPStatus.NOT_FOUND,
)
with pytest.raises(
expected_exception=ServiceValidationError,
match=r"Unable to update the score for your Habitica to-do `.+`, please try again",
):
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", "uid", "date"),
[
(
"todo.test_user_to_do_s",
"88de7cd9-af2b-49ce-9afd-bf941d87336b",
"2024-07-30",
),
(
"todo.test_user_dailies",
"f2c85972-1a19-4426-bc6d-ce3337b9d99f",
None,
),
],
ids=["todo", "daily"],
)
async def test_update_todo_item(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
entity_id: str,
uid: str,
date: str,
) -> None:
"""Test update details of a 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
mock_habitica.put(
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
json={"data": {}, "success": True},
)
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.UPDATE_ITEM,
{
ATTR_ITEM: uid,
ATTR_RENAME: "test-summary",
ATTR_DESCRIPTION: "test-description",
ATTR_DUE_DATE: date,
},
target={ATTR_ENTITY_ID: entity_id},
blocking=True,
)
mock_call = mock_called_with(
mock_habitica, "PUT", f"{DEFAULT_URL}/api/v3/tasks/{uid}"
)
assert mock_call
assert json.loads(mock_call[2]) == {
"date": date,
"notes": "test-description",
"text": "test-summary",
}
async def test_update_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.put(
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
status=HTTPStatus.NOT_FOUND,
)
with pytest.raises(
expected_exception=ServiceValidationError,
match="Unable to update the Habitica to-do `test-summary`, please try again",
):
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,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/user",
json={"data": {}, "success": True},
status=HTTPStatus.CREATED,
)
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,
)
mock_call = mock_called_with(
mock_habitica,
"post",
f"{DEFAULT_URL}/api/v3/tasks/user",
)
assert mock_call
assert json.loads(mock_call[2]) == {
"date": "2024-07-30",
"notes": "test-description",
"text": "test-summary",
"type": "todo",
}
async def test_add_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/user",
status=HTTPStatus.NOT_FOUND,
)
with pytest.raises(
expected_exception=ServiceValidationError,
match="Unable to create new to-do `test-summary` for Habitica, please try again",
):
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,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.delete(
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
json={"data": {}, "success": True},
)
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,
)
assert mock_called_with(
mock_habitica, "delete", f"{DEFAULT_URL}/api/v3/tasks/{uid}"
)
async def test_delete_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.delete(
f"{DEFAULT_URL}/api/v3/tasks/{uid}",
status=HTTPStatus.NOT_FOUND,
)
with pytest.raises(
expected_exception=ServiceValidationError,
match="Unable to delete item from Habitica to-do list, please try again",
):
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,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/clearCompletedTodos",
json={"data": {}, "success": True},
)
await hass.services.async_call(
TODO_DOMAIN,
TodoServices.REMOVE_COMPLETED_ITEMS,
{},
target={ATTR_ENTITY_ID: "todo.test_user_to_do_s"},
blocking=True,
)
assert mock_called_with(
mock_habitica, "post", f"{DEFAULT_URL}/api/v3/tasks/clearCompletedTodos"
)
async def test_delete_completed_todo_items_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
) -> 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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/clearCompletedTodos",
status=HTTPStatus.NOT_FOUND,
)
with pytest.raises(
expected_exception=ServiceValidationError,
match="Unable to delete completed to-do items from Habitica to-do list, please try again",
):
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,
mock_habitica: AiohttpClientMocker,
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
for pos in (0, 1):
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/{uid}/move/to/{pos}",
json={"data": {}, "success": True},
)
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")
# 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")
for pos in (0, 1):
assert mock_called_with(
mock_habitica,
"post",
f"{DEFAULT_URL}/api/v3/tasks/{uid}/move/to/{pos}",
)
async def test_move_todo_item_exception(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
hass_ws_client: WebSocketGenerator,
) -> 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
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/tasks/{uid}/move/to/0",
status=HTTPStatus.NOT_FOUND,
)
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()
assert resp.get("success") is False
@pytest.mark.parametrize(
("fixture", "calculated_due_date"),
[
("duedate_fixture_1.json", (2024, 9, 23)),
("duedate_fixture_2.json", (2024, 9, 24)),
("duedate_fixture_3.json", (2024, 10, 23)),
("duedate_fixture_4.json", (2024, 10, 23)),
("duedate_fixture_5.json", (2024, 9, 28)),
("duedate_fixture_6.json", (2024, 10, 21)),
("duedate_fixture_7.json", None),
("duedate_fixture_8.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",
],
)
@pytest.mark.usefixtures("set_tz")
async def test_next_due_date(
hass: HomeAssistant,
fixture: str,
calculated_due_date: tuple | None,
config_entry: MockConfigEntry,
aioclient_mock: AiohttpClientMocker,
) -> None:
"""Test next_due_date calculation."""
dailies_entity = "todo.test_user_dailies"
aioclient_mock.get(
f"{DEFAULT_URL}/api/v3/user", json=load_json_object_fixture("user.json", DOMAIN)
)
aioclient_mock.get(
f"{DEFAULT_URL}/api/v3/tasks/user",
params={"type": "completedTodos"},
json={"data": []},
)
aioclient_mock.get(
f"{DEFAULT_URL}/api/v3/tasks/user",
json=load_json_object_fixture(fixture, DOMAIN),
)
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") is None
if not calculated_due_date
else datetime(*calculated_due_date).date()
)