Improve exception handling in Habitica integration (#135950)

This commit is contained in:
Manu 2025-01-19 19:51:55 +01:00 committed by GitHub
parent ccd7b1c21a
commit ec45cb4939
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 337 additions and 56 deletions

View File

@ -335,16 +335,24 @@ class HabiticaButton(HabiticaBase, ButtonEntity):
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except NotAuthorizedError as e: except NotAuthorizedError as e:
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_unallowed", translation_key="service_call_unallowed",
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": e.error.message},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
else: else:
await self.coordinator.async_request_refresh() await self.coordinator.async_request_refresh()

View File

@ -85,11 +85,19 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise ConfigEntryNotReady( raise ConfigEntryNotReady(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise ConfigEntryNotReady(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
if not self.config_entry.data.get(CONF_NAME): if not self.config_entry.data.get(CONF_NAME):
@ -108,8 +116,18 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
except TooManyRequestsError: except TooManyRequestsError:
_LOGGER.debug("Rate limit exceeded, will try again later") _LOGGER.debug("Rate limit exceeded, will try again later")
return self.data return self.data
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise UpdateFailed(f"Unable to connect to Habitica: {e}") from e raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise UpdateFailed(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e
else: else:
return HabiticaData(user=user, tasks=tasks + completed_todos) return HabiticaData(user=user, tasks=tasks + completed_todos)
@ -124,11 +142,19 @@ class HabiticaDataUpdateCoordinator(DataUpdateCoordinator[HabiticaData]):
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": e.error.message},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
else: else:
await self.async_request_refresh() await self.async_request_refresh()

View File

@ -64,9 +64,7 @@ rules:
entity-device-class: done entity-device-class: done
entity-disabled-by-default: done entity-disabled-by-default: done
entity-translations: done entity-translations: done
exception-translations: exception-translations: done
status: todo
comment: translations for UpdateFailed missing
icon-translations: done icon-translations: done
reconfiguration-flow: todo reconfiguration-flow: todo
repair-issues: repair-issues:

View File

@ -224,6 +224,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except NotAuthorizedError as e: except NotAuthorizedError as e:
raise ServiceValidationError( raise ServiceValidationError(
@ -243,10 +244,17 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
translation_key="skill_not_found", translation_key="skill_not_found",
translation_placeholders={"skill": call.data[ATTR_SKILL]}, translation_placeholders={"skill": call.data[ATTR_SKILL]},
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
else: else:
await coordinator.async_request_refresh() await coordinator.async_request_refresh()
@ -274,6 +282,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except NotAuthorizedError as e: except NotAuthorizedError as e:
raise ServiceValidationError( raise ServiceValidationError(
@ -283,9 +292,17 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_key="quest_not_found" translation_domain=DOMAIN, translation_key="quest_not_found"
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_key="service_call_exception" translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
else: else:
return asdict(response.data) return asdict(response.data)
@ -335,6 +352,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except NotAuthorizedError as e: except NotAuthorizedError as e:
if task_value is not None: if task_value is not None:
@ -349,11 +367,19 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": e.error.message},
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
else: else:
await coordinator.async_request_refresh() await coordinator.async_request_refresh()
@ -382,10 +408,17 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="party_not_found", translation_key="party_not_found",
) from e ) from e
except (ClientError, HabiticaException) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
try: try:
target_id = next( target_id = next(
@ -411,6 +444,7 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception", translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e ) from e
except NotAuthorizedError as e: except NotAuthorizedError as e:
raise ServiceValidationError( raise ServiceValidationError(
@ -418,10 +452,17 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
translation_key="item_not_found", translation_key="item_not_found",
translation_placeholders={"item": call.data[ATTR_ITEM]}, translation_placeholders={"item": call.data[ATTR_ITEM]},
) from e ) from e
except (HabiticaException, ClientError) as e: except HabiticaException as e:
raise HomeAssistantError( raise HomeAssistantError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="service_call_exception", translation_key="service_call_exception",
translation_placeholders={"reason": str(e.error.message)},
) from e
except ClientError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
translation_placeholders={"reason": str(e)},
) from e ) from e
else: else:
return asdict(response.data) return asdict(response.data)

View File

@ -375,13 +375,13 @@
"message": "Unable to create new to-do `{name}` for Habitica, please try again" "message": "Unable to create new to-do `{name}` for Habitica, please try again"
}, },
"setup_rate_limit_exception": { "setup_rate_limit_exception": {
"message": "Rate limit exceeded, try again later" "message": "Rate limit exceeded, try again in {retry_after} seconds"
}, },
"service_call_unallowed": { "service_call_unallowed": {
"message": "Unable to complete action, the required conditions are not met" "message": "Unable to complete action, the required conditions are not met"
}, },
"service_call_exception": { "service_call_exception": {
"message": "Unable to connect to Habitica, try again later" "message": "Unable to connect to Habitica: {reason}"
}, },
"not_enough_mana": { "not_enough_mana": {
"message": "Unable to cast skill, not enough mana. Your character has {mana}, but the skill costs {cost}." "message": "Unable to cast skill, not enough mana. Your character has {mana}, but the skill costs {cost}."

View File

@ -3,11 +3,18 @@
from __future__ import annotations from __future__ import annotations
from enum import StrEnum from enum import StrEnum
import logging
from typing import TYPE_CHECKING from typing import TYPE_CHECKING
from uuid import UUID from uuid import UUID
from aiohttp import ClientError from aiohttp import ClientError
from habiticalib import Direction, HabiticaException, Task, TaskType from habiticalib import (
Direction,
HabiticaException,
Task,
TaskType,
TooManyRequestsError,
)
from homeassistant.components import persistent_notification from homeassistant.components import persistent_notification
from homeassistant.components.todo import ( from homeassistant.components.todo import (
@ -17,7 +24,7 @@ from homeassistant.components.todo import (
TodoListEntityFeature, TodoListEntityFeature,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers.entity import EntityDescription from homeassistant.helpers.entity import EntityDescription
from homeassistant.helpers.entity_platform import AddEntitiesCallback from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.util import dt as dt_util from homeassistant.util import dt as dt_util
@ -28,6 +35,8 @@ from .entity import HabiticaBase
from .types import HabiticaConfigEntry from .types import HabiticaConfigEntry
from .util import next_due_date from .util import next_due_date
_LOGGER = logging.getLogger(__name__)
PARALLEL_UPDATES = 1 PARALLEL_UPDATES = 1
@ -72,7 +81,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
if len(uids) > 1 and self.entity_description.key is HabiticaTodoList.TODOS: if len(uids) > 1 and self.entity_description.key is HabiticaTodoList.TODOS:
try: try:
await self.coordinator.habitica.delete_completed_todos() await self.coordinator.habitica.delete_completed_todos()
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e
except (HabiticaException, ClientError) as e: except (HabiticaException, ClientError) as e:
_LOGGER.debug(str(e))
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="delete_completed_todos_failed", translation_key="delete_completed_todos_failed",
@ -81,7 +97,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
for task_id in uids: for task_id in uids:
try: try:
await self.coordinator.habitica.delete_task(UUID(task_id)) await self.coordinator.habitica.delete_task(UUID(task_id))
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e
except (HabiticaException, ClientError) as e: except (HabiticaException, ClientError) as e:
_LOGGER.debug(str(e))
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"delete_{self.entity_description.key}_failed", translation_key=f"delete_{self.entity_description.key}_failed",
@ -108,7 +131,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
try: try:
await self.coordinator.habitica.reorder_task(UUID(uid), pos) await self.coordinator.habitica.reorder_task(UUID(uid), pos)
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e
except (HabiticaException, ClientError) as e: except (HabiticaException, ClientError) as e:
_LOGGER.debug(str(e))
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"move_{self.entity_description.key}_item_failed", translation_key=f"move_{self.entity_description.key}_item_failed",
@ -160,7 +190,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
try: try:
await self.coordinator.habitica.update_task(UUID(item.uid), task) await self.coordinator.habitica.update_task(UUID(item.uid), task)
refresh_required = True refresh_required = True
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e
except (HabiticaException, ClientError) as e: except (HabiticaException, ClientError) as e:
_LOGGER.debug(str(e))
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"update_{self.entity_description.key}_item_failed", translation_key=f"update_{self.entity_description.key}_item_failed",
@ -187,8 +224,14 @@ class BaseHabiticaListEntity(HabiticaBase, TodoListEntity):
refresh_required = True refresh_required = True
else: else:
score_result = None score_result = None
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e
except (HabiticaException, ClientError) as e: except (HabiticaException, ClientError) as e:
_LOGGER.debug(str(e))
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"score_{self.entity_description.key}_item_failed", translation_key=f"score_{self.entity_description.key}_item_failed",
@ -260,7 +303,14 @@ class HabiticaTodosListEntity(BaseHabiticaListEntity):
date=item.due, date=item.due,
) )
) )
except TooManyRequestsError as e:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
translation_placeholders={"retry_after": str(e.retry_after)},
) from e
except (HabiticaException, ClientError) as e: except (HabiticaException, ClientError) as e:
_LOGGER.debug(str(e))
raise ServiceValidationError( raise ServiceValidationError(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key=f"create_{self.entity_description.key}_item_failed", translation_key=f"create_{self.entity_description.key}_item_failed",

View File

@ -32,11 +32,13 @@ from homeassistant.core import HomeAssistant
from tests.common import MockConfigEntry, load_fixture from tests.common import MockConfigEntry, load_fixture
ERROR_RESPONSE = HabiticaErrorResponse(success=False, error="error", message="message") ERROR_RESPONSE = HabiticaErrorResponse(success=False, error="error", message="reason")
ERROR_NOT_AUTHORIZED = NotAuthorizedError(error=ERROR_RESPONSE, headers={}) ERROR_NOT_AUTHORIZED = NotAuthorizedError(error=ERROR_RESPONSE, headers={})
ERROR_NOT_FOUND = NotFoundError(error=ERROR_RESPONSE, headers={}) ERROR_NOT_FOUND = NotFoundError(error=ERROR_RESPONSE, headers={})
ERROR_BAD_REQUEST = BadRequestError(error=ERROR_RESPONSE, headers={}) ERROR_BAD_REQUEST = BadRequestError(error=ERROR_RESPONSE, headers={})
ERROR_TOO_MANY_REQUESTS = TooManyRequestsError(error=ERROR_RESPONSE, headers={}) ERROR_TOO_MANY_REQUESTS = TooManyRequestsError(
error=ERROR_RESPONSE, headers={"retry-after": 5}
)
@pytest.fixture(name="config_entry") @pytest.fixture(name="config_entry")

View File

@ -4,6 +4,7 @@ from collections.abc import Generator
from datetime import timedelta from datetime import timedelta
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohttp import ClientError
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
from habiticalib import HabiticaUserResponse, Skill from habiticalib import HabiticaUserResponse, Skill
import pytest import pytest
@ -215,12 +216,12 @@ async def test_button_press(
[ [
( (
ERROR_TOO_MANY_REQUESTS, ERROR_TOO_MANY_REQUESTS,
"Rate limit exceeded, try again later", "Rate limit exceeded, try again in 5 seconds",
HomeAssistantError, HomeAssistantError,
), ),
( (
ERROR_BAD_REQUEST, ERROR_BAD_REQUEST,
"Unable to connect to Habitica, try again later", "Unable to connect to Habitica: reason",
HomeAssistantError, HomeAssistantError,
), ),
( (
@ -228,6 +229,11 @@ async def test_button_press(
"Unable to complete action, the required conditions are not met", "Unable to complete action, the required conditions are not met",
ServiceValidationError, ServiceValidationError,
), ),
(
ClientError,
"Unable to connect to Habitica: ",
HomeAssistantError,
),
], ],
) )
async def test_button_press_exceptions( async def test_button_press_exceptions(

View File

@ -4,6 +4,7 @@ import datetime
import logging import logging
from unittest.mock import AsyncMock from unittest.mock import AsyncMock
from aiohttp import ClientError
from freezegun.api import FrozenDateTimeFactory from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
@ -85,11 +86,12 @@ async def test_service_call(
@pytest.mark.parametrize( @pytest.mark.parametrize(
("exception"), ("exception"),
[ [ERROR_BAD_REQUEST, ERROR_TOO_MANY_REQUESTS, ClientError],
ERROR_BAD_REQUEST, ids=[
ERROR_TOO_MANY_REQUESTS, "BadRequestError",
"TooManyRequestsError",
"ClientError",
], ],
ids=["BadRequestError", "TooManyRequestsError"],
) )
async def test_config_entry_not_ready( async def test_config_entry_not_ready(
hass: HomeAssistant, hass: HomeAssistant,
@ -131,14 +133,16 @@ async def test_config_entry_auth_failed(
assert flow["context"].get("entry_id") == config_entry.entry_id assert flow["context"].get("entry_id") == config_entry.entry_id
@pytest.mark.parametrize("exception", [ERROR_NOT_FOUND, ClientError])
async def test_coordinator_update_failed( async def test_coordinator_update_failed(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
exception: Exception,
) -> None: ) -> None:
"""Test coordinator update failed.""" """Test coordinator update failed."""
habitica.get_tasks.side_effect = ERROR_NOT_FOUND habitica.get_tasks.side_effect = exception
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()

View File

@ -5,6 +5,7 @@ from typing import Any
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from uuid import UUID from uuid import UUID
from aiohttp import ClientError
from habiticalib import Direction, Skill from habiticalib import Direction, Skill
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -46,8 +47,8 @@ from .conftest import (
from tests.common import MockConfigEntry from tests.common import MockConfigEntry
REQUEST_EXCEPTION_MSG = "Unable to connect to Habitica, try again later" REQUEST_EXCEPTION_MSG = "Unable to connect to Habitica: reason"
RATE_LIMIT_EXCEPTION_MSG = "Rate limit exceeded, try again later" RATE_LIMIT_EXCEPTION_MSG = "Rate limit exceeded, try again in 5 seconds"
@pytest.fixture(autouse=True) @pytest.fixture(autouse=True)
@ -235,6 +236,15 @@ async def test_cast_skill(
HomeAssistantError, HomeAssistantError,
REQUEST_EXCEPTION_MSG, REQUEST_EXCEPTION_MSG,
), ),
(
{
ATTR_TASK: "Rechnungen bezahlen",
ATTR_SKILL: "smash",
},
ClientError,
HomeAssistantError,
"Unable to connect to Habitica: ",
),
], ],
) )
async def test_cast_skill_exceptions( async def test_cast_skill_exceptions(
@ -360,6 +370,11 @@ async def test_handle_quests(
HomeAssistantError, HomeAssistantError,
REQUEST_EXCEPTION_MSG, REQUEST_EXCEPTION_MSG,
), ),
(
ClientError,
HomeAssistantError,
"Unable to connect to Habitica: ",
),
], ],
) )
@pytest.mark.parametrize( @pytest.mark.parametrize(
@ -520,6 +535,15 @@ async def test_score_task(
HomeAssistantError, HomeAssistantError,
REQUEST_EXCEPTION_MSG, REQUEST_EXCEPTION_MSG,
), ),
(
{
ATTR_TASK: "e97659e0-2c42-4599-a7bb-00282adc410d",
ATTR_DIRECTION: "up",
},
ClientError,
HomeAssistantError,
"Unable to connect to Habitica: ",
),
( (
{ {
ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b", ATTR_TASK: "5e2ea1df-f6e6-4ba3-bccb-97c5ec63e99b",
@ -722,7 +746,7 @@ async def test_transformation(
ERROR_BAD_REQUEST, ERROR_BAD_REQUEST,
None, None,
HomeAssistantError, HomeAssistantError,
"Unable to connect to Habitica, try again later", REQUEST_EXCEPTION_MSG,
), ),
( (
{ {
@ -752,7 +776,27 @@ async def test_transformation(
None, None,
ERROR_BAD_REQUEST, ERROR_BAD_REQUEST,
HomeAssistantError, HomeAssistantError,
"Unable to connect to Habitica, try again later", REQUEST_EXCEPTION_MSG,
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
None,
ClientError,
HomeAssistantError,
"Unable to connect to Habitica: ",
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
ClientError,
None,
HomeAssistantError,
"Unable to connect to Habitica: ",
), ),
], ],
) )

View File

@ -3,6 +3,7 @@
from collections.abc import Generator from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
from aiohttp import ClientError
import pytest import pytest
from syrupy.assertion import SnapshotAssertion from syrupy.assertion import SnapshotAssertion
@ -96,6 +97,7 @@ async def test_turn_on_off_toggle(
[ [
(ERROR_TOO_MANY_REQUESTS, HomeAssistantError), (ERROR_TOO_MANY_REQUESTS, HomeAssistantError),
(ERROR_BAD_REQUEST, HomeAssistantError), (ERROR_BAD_REQUEST, HomeAssistantError),
(ClientError, HomeAssistantError),
], ],
) )
async def test_turn_on_off_toggle_exceptions( async def test_turn_on_off_toggle_exceptions(

View File

@ -23,10 +23,10 @@ from homeassistant.components.todo import (
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ATTR_ENTITY_ID, Platform from homeassistant.const import ATTR_ENTITY_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from .conftest import ERROR_NOT_FOUND from .conftest import ERROR_NOT_FOUND, ERROR_TOO_MANY_REQUESTS
from tests.common import ( from tests.common import (
MockConfigEntry, MockConfigEntry,
@ -183,12 +183,30 @@ async def test_uncomplete_todo_item(
], ],
ids=["completed", "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( async def test_complete_todo_item_exception(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
uid: str, uid: str,
status: str, status: str,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None: ) -> None:
"""Test exception when completing/uncompleting an item on the todo list.""" """Test exception when completing/uncompleting an item on the todo list."""
@ -198,10 +216,10 @@ async def test_complete_todo_item_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
habitica.update_score.side_effect = ERROR_NOT_FOUND habitica.update_score.side_effect = exception
with pytest.raises( with pytest.raises(
expected_exception=ServiceValidationError, expected_exception=expected_exception,
match=r"Unable to update the score for your Habitica to-do `.+`, please try again", match=exc_msg,
): ):
await hass.services.async_call( await hass.services.async_call(
TODO_DOMAIN, TODO_DOMAIN,
@ -311,10 +329,28 @@ async def test_update_todo_item(
habitica.update_task.assert_awaited_once_with(*call_args) 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( async def test_update_todo_item_exception(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None: ) -> None:
"""Test exception when update item on the todo list.""" """Test exception when update item on the todo list."""
uid = "88de7cd9-af2b-49ce-9afd-bf941d87336b" uid = "88de7cd9-af2b-49ce-9afd-bf941d87336b"
@ -324,11 +360,8 @@ async def test_update_todo_item_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
habitica.update_task.side_effect = ERROR_NOT_FOUND habitica.update_task.side_effect = exception
with pytest.raises( with pytest.raises(expected_exception=expected_exception, match=exc_msg):
expected_exception=ServiceValidationError,
match="Unable to update the Habitica to-do `test-summary`, please try again",
):
await hass.services.async_call( await hass.services.async_call(
TODO_DOMAIN, TODO_DOMAIN,
TodoServices.UPDATE_ITEM, TodoServices.UPDATE_ITEM,
@ -378,10 +411,28 @@ async def test_add_todo_item(
) )
@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( async def test_add_todo_item_exception(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None: ) -> None:
"""Test exception when adding a todo item to the todo list.""" """Test exception when adding a todo item to the todo list."""
@ -391,10 +442,11 @@ async def test_add_todo_item_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
habitica.create_task.side_effect = ERROR_NOT_FOUND habitica.create_task.side_effect = exception
with pytest.raises( with pytest.raises(
expected_exception=ServiceValidationError, expected_exception=expected_exception,
match="Unable to create new to-do `test-summary` for Habitica, please try again", # match="Unable to create new to-do `test-summary` for Habitica, please try again",
match=exc_msg,
): ):
await hass.services.async_call( await hass.services.async_call(
TODO_DOMAIN, TODO_DOMAIN,
@ -434,10 +486,28 @@ async def test_delete_todo_item(
habitica.delete_task.assert_awaited_once_with(UUID(uid)) 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( async def test_delete_todo_item_exception(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None: ) -> None:
"""Test exception when deleting a todo item from the todo list.""" """Test exception when deleting a todo item from the todo list."""
@ -448,11 +518,11 @@ async def test_delete_todo_item_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
habitica.delete_task.side_effect = ERROR_NOT_FOUND habitica.delete_task.side_effect = exception
with pytest.raises( with pytest.raises(
expected_exception=ServiceValidationError, expected_exception=expected_exception,
match="Unable to delete item from Habitica to-do list, please try again", match=exc_msg,
): ):
await hass.services.async_call( await hass.services.async_call(
TODO_DOMAIN, TODO_DOMAIN,
@ -486,10 +556,28 @@ async def test_delete_completed_todo_items(
habitica.delete_completed_todos.assert_awaited_once() 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( async def test_delete_completed_todo_items_exception(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
exception: Exception,
exc_msg: str,
expected_exception: Exception,
) -> None: ) -> None:
"""Test exception when deleting completed todo items from the todo list.""" """Test exception when deleting completed todo items from the todo list."""
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
@ -498,10 +586,10 @@ async def test_delete_completed_todo_items_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
habitica.delete_completed_todos.side_effect = ERROR_NOT_FOUND habitica.delete_completed_todos.side_effect = exception
with pytest.raises( with pytest.raises(
expected_exception=ServiceValidationError, expected_exception=expected_exception,
match="Unable to delete completed to-do items from Habitica to-do list, please try again", match=exc_msg,
): ):
await hass.services.async_call( await hass.services.async_call(
TODO_DOMAIN, TODO_DOMAIN,
@ -575,11 +663,26 @@ async def test_move_todo_item(
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0) 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( async def test_move_todo_item_exception(
hass: HomeAssistant, hass: HomeAssistant,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
habitica: AsyncMock, habitica: AsyncMock,
hass_ws_client: WebSocketGenerator, hass_ws_client: WebSocketGenerator,
exception: Exception,
exc_msg: str,
) -> None: ) -> None:
"""Test exception when moving todo item.""" """Test exception when moving todo item."""
@ -590,7 +693,7 @@ async def test_move_todo_item_exception(
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
habitica.reorder_task.side_effect = ERROR_NOT_FOUND habitica.reorder_task.side_effect = exception
client = await hass_ws_client() client = await hass_ws_client()
data = { data = {
@ -605,10 +708,7 @@ async def test_move_todo_item_exception(
habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0) habitica.reorder_task.assert_awaited_once_with(UUID(uid), 0)
assert resp["success"] is False assert resp["success"] is False
assert ( assert resp["error"]["message"] == exc_msg
resp["error"]["message"]
== "Unable to move the Habitica to-do to position 0, please try again"
)
@pytest.mark.parametrize( @pytest.mark.parametrize(