Add action for using transformation items to Habitica (#129606)

This commit is contained in:
Manu 2024-11-15 17:38:30 +01:00 committed by GitHub
parent 50cc6b4e01
commit e26142949d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 849 additions and 1 deletions

View File

@ -26,6 +26,8 @@ ATTR_CONFIG_ENTRY = "config_entry"
ATTR_SKILL = "skill"
ATTR_TASK = "task"
ATTR_DIRECTION = "direction"
ATTR_TARGET = "target"
ATTR_ITEM = "item"
SERVICE_CAST_SKILL = "cast_skill"
SERVICE_START_QUEST = "start_quest"
SERVICE_ACCEPT_QUEST = "accept_quest"
@ -36,6 +38,9 @@ SERVICE_LEAVE_QUEST = "leave_quest"
SERVICE_SCORE_HABIT = "score_habit"
SERVICE_SCORE_REWARD = "score_reward"
SERVICE_TRANSFORMATION = "transformation"
WARRIOR = "warrior"
ROGUE = "rogue"
HEALER = "healer"

View File

@ -187,6 +187,9 @@
},
"score_reward": {
"service": "mdi:sack"
},
"transformation": {
"service": "mdi:flask-round-bottom"
}
}
}

View File

@ -27,8 +27,10 @@ from .const import (
ATTR_CONFIG_ENTRY,
ATTR_DATA,
ATTR_DIRECTION,
ATTR_ITEM,
ATTR_PATH,
ATTR_SKILL,
ATTR_TARGET,
ATTR_TASK,
DOMAIN,
EVENT_API_CALL_SUCCESS,
@ -42,6 +44,7 @@ from .const import (
SERVICE_SCORE_HABIT,
SERVICE_SCORE_REWARD,
SERVICE_START_QUEST,
SERVICE_TRANSFORMATION,
)
from .types import HabiticaConfigEntry
@ -77,6 +80,14 @@ SERVICE_SCORE_TASK_SCHEMA = vol.Schema(
}
)
SERVICE_TRANSFORMATION_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY): ConfigEntrySelector(),
vol.Required(ATTR_ITEM): cv.string,
vol.Required(ATTR_TARGET): cv.string,
}
)
def get_config_entry(hass: HomeAssistant, entry_id: str) -> HabiticaConfigEntry:
"""Return config entry or raise if not found or not loaded."""
@ -294,6 +305,83 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
await coordinator.async_request_refresh()
return response
async def transformation(call: ServiceCall) -> ServiceResponse:
"""User a transformation item on a player character."""
entry = get_config_entry(hass, call.data[ATTR_CONFIG_ENTRY])
coordinator = entry.runtime_data
ITEMID_MAP = {
"snowball": {"itemId": "snowball"},
"spooky_sparkles": {"itemId": "spookySparkles"},
"seafoam": {"itemId": "seafoam"},
"shiny_seed": {"itemId": "shinySeed"},
}
# check if target is self
if call.data[ATTR_TARGET] in (
coordinator.data.user["id"],
coordinator.data.user["profile"]["name"],
coordinator.data.user["auth"]["local"]["username"],
):
target_id = coordinator.data.user["id"]
else:
# check if target is a party member
try:
party = await coordinator.api.groups.party.members.get()
except ClientResponseError as e:
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
) from e
if e.status == HTTPStatus.NOT_FOUND:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="party_not_found",
) from e
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
) from e
try:
target_id = next(
member["id"]
for member in party
if call.data[ATTR_TARGET].lower()
in (
member["id"],
member["auth"]["local"]["username"].lower(),
member["profile"]["name"].lower(),
)
)
except StopIteration as e:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="target_not_found",
translation_placeholders={"target": f"'{call.data[ATTR_TARGET]}'"},
) from e
try:
response: dict[str, Any] = await coordinator.api.user.class_.cast[
ITEMID_MAP[call.data[ATTR_ITEM]]["itemId"]
].post(targetId=target_id)
except ClientResponseError as e:
if e.status == HTTPStatus.TOO_MANY_REQUESTS:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="setup_rate_limit_exception",
) from e
if e.status == HTTPStatus.UNAUTHORIZED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="item_not_found",
translation_placeholders={"item": call.data[ATTR_ITEM]},
) from e
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="service_call_exception",
) from e
else:
return response
hass.services.async_register(
DOMAIN,
SERVICE_API_CALL,
@ -323,3 +411,11 @@ def async_setup_services(hass: HomeAssistant) -> None: # noqa: C901
schema=SERVICE_SCORE_TASK_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
hass.services.async_register(
DOMAIN,
SERVICE_TRANSFORMATION,
transformation,
schema=SERVICE_TRANSFORMATION_SCHEMA,
supports_response=SupportsResponse.ONLY,
)

View File

@ -72,3 +72,25 @@ score_reward:
fields:
config_entry: *config_entry
task: *task
transformation:
fields:
config_entry:
required: true
selector:
config_entry:
integration: habitica
item:
required: true
selector:
select:
options:
- "snowball"
- "spooky_sparkles"
- "seafoam"
- "shiny_seed"
mode: dropdown
translation_key: "transformation_item_select"
target:
required: true
selector:
text:

View File

@ -321,6 +321,15 @@
},
"quest_not_found": {
"message": "Unable to complete action, quest or group not found"
},
"target_not_found": {
"message": "Unable to find target {target} in your party"
},
"party_not_found": {
"message": "Unable to find target, you are currently not in a party. You can only target yourself"
},
"item_not_found": {
"message": "Unable to use {item}, you don't own this item."
}
},
"issues": {
@ -461,6 +470,24 @@
"description": "The name (or task ID) of the custom reward."
}
}
},
"transformation": {
"name": "Use a transformation item",
"description": "Use a transformation item from your Habitica character's inventory on a member of your party or yourself.",
"fields": {
"config_entry": {
"name": "Select character",
"description": "Choose the Habitica character to use the transformation item."
},
"item": {
"name": "Transformation item",
"description": "Select the transformation item you want to use. Item must be in the characters inventory."
},
"target": {
"name": "Target character",
"description": "The name of the character you want to use the transformation item on. You can also specify the players username or user ID."
}
}
}
},
"selector": {
@ -471,6 +498,14 @@
"backstab": "Rogue: Backstab",
"smash": "Warrior: Brutal smash"
}
},
"transformation_item_select": {
"options": {
"snowball": "Snowball",
"spooky_sparkles": "Spooky sparkles",
"seafoam": "Seafoam",
"shiny_seed": "Shiny seed"
}
}
}
}

View File

@ -0,0 +1,442 @@
{
"success": true,
"data": [
{
"_id": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
"auth": {
"local": {
"username": "test-username"
},
"timestamps": {
"created": "2024-10-19T18:43:39.782Z",
"loggedin": "2024-10-31T16:13:35.048Z",
"updated": "2024-10-31T16:15:56.552Z"
}
},
"achievements": {
"ultimateGearSets": {
"healer": false,
"wizard": false,
"rogue": false,
"warrior": false
},
"streak": 0,
"challenges": [],
"perfect": 1,
"quests": {},
"purchasedEquipment": true,
"completedTask": true,
"partyUp": true
},
"backer": {},
"contributor": {},
"flags": {
"verifiedUsername": true,
"classSelected": true
},
"items": {
"gear": {
"owned": {
"headAccessory_special_blackHeadband": true,
"headAccessory_special_blueHeadband": true,
"headAccessory_special_greenHeadband": true,
"headAccessory_special_pinkHeadband": true,
"headAccessory_special_redHeadband": true,
"headAccessory_special_whiteHeadband": true,
"headAccessory_special_yellowHeadband": true,
"eyewear_special_blackTopFrame": true,
"eyewear_special_blueTopFrame": true,
"eyewear_special_greenTopFrame": true,
"eyewear_special_pinkTopFrame": true,
"eyewear_special_redTopFrame": true,
"eyewear_special_whiteTopFrame": true,
"eyewear_special_yellowTopFrame": true,
"eyewear_special_blackHalfMoon": true,
"eyewear_special_blueHalfMoon": true,
"eyewear_special_greenHalfMoon": true,
"eyewear_special_pinkHalfMoon": true,
"eyewear_special_redHalfMoon": true,
"eyewear_special_whiteHalfMoon": true,
"eyewear_special_yellowHalfMoon": true,
"armor_special_bardRobes": true,
"weapon_special_fall2024Warrior": true,
"shield_special_fall2024Warrior": true,
"head_special_fall2024Warrior": true,
"armor_special_fall2024Warrior": true,
"back_mystery_201402": true,
"body_mystery_202003": true,
"head_special_bardHat": true,
"weapon_wizard_0": true
},
"equipped": {
"weapon": "weapon_special_fall2024Warrior",
"armor": "armor_special_fall2024Warrior",
"head": "head_special_fall2024Warrior",
"shield": "shield_special_fall2024Warrior",
"back": "back_mystery_201402",
"headAccessory": "headAccessory_special_pinkHeadband",
"eyewear": "eyewear_special_pinkHalfMoon",
"body": "body_mystery_202003"
},
"costume": {
"armor": "armor_base_0",
"head": "head_base_0",
"shield": "shield_base_0"
}
},
"special": {
"snowball": 99,
"spookySparkles": 99,
"shinySeed": 99,
"seafoam": 99,
"valentine": 0,
"valentineReceived": [],
"nye": 0,
"nyeReceived": [],
"greeting": 0,
"greetingReceived": [],
"thankyou": 0,
"thankyouReceived": [],
"birthday": 0,
"birthdayReceived": [],
"congrats": 0,
"congratsReceived": [],
"getwell": 0,
"getwellReceived": [],
"goodluck": 0,
"goodluckReceived": []
},
"pets": {
"Rat-Shade": 1,
"Gryphatrice-Jubilant": 1
},
"currentPet": "Gryphatrice-Jubilant",
"eggs": {
"Cactus": 1,
"Fox": 2,
"Wolf": 1
},
"hatchingPotions": {
"CottonCandyBlue": 1,
"RoyalPurple": 1
},
"food": {
"Meat": 2,
"Chocolate": 1,
"CottonCandyPink": 1,
"Candy_Zombie": 1
},
"mounts": {
"Velociraptor-Base": true,
"Gryphon-Gryphatrice": true
},
"currentMount": "Gryphon-Gryphatrice",
"quests": {
"dustbunnies": 1,
"vice1": 1,
"atom1": 1,
"moonstone1": 1,
"goldenknight1": 1,
"basilist": 1
},
"lastDrop": {
"date": "2024-10-31T16:13:34.952Z",
"count": 0
}
},
"party": {
"quest": {
"progress": {
"up": 0,
"down": 0,
"collectedItems": 0,
"collect": {}
},
"RSVPNeeded": false,
"key": "dustbunnies"
},
"order": "level",
"orderAscending": "ascending",
"_id": "94cd398c-2240-4320-956e-6d345cf2c0de"
},
"preferences": {
"size": "slim",
"hair": {
"color": "red",
"base": 3,
"bangs": 1,
"beard": 0,
"mustache": 0,
"flower": 1
},
"skin": "915533",
"shirt": "blue",
"chair": "handleless_pink",
"costume": false,
"sleep": false,
"disableClasses": false,
"tasks": {
"groupByChallenge": false,
"confirmScoreNotes": false,
"mirrorGroupTasks": [],
"activeFilter": {
"habit": "all",
"daily": "all",
"todo": "remaining",
"reward": "all"
}
},
"background": "violet"
},
"profile": {
"name": "test-user"
},
"stats": {
"hp": 50,
"mp": 150.8,
"exp": 127,
"gp": 19.08650199252128,
"lvl": 99,
"class": "wizard",
"points": 0,
"str": 0,
"con": 0,
"int": 0,
"per": 0,
"buffs": {
"str": 50,
"int": 50,
"per": 50,
"con": 50,
"stealth": 0,
"streaks": false,
"seafoam": false,
"shinySeed": false,
"snowball": false,
"spookySparkles": false
},
"training": {
"int": 0,
"per": 0,
"str": 0,
"con": 0
},
"toNextLevel": 3580,
"maxHealth": 50,
"maxMP": 228
},
"inbox": {
"optOut": false
},
"loginIncentives": 6,
"id": "a380546a-94be-4b8e-8a0b-23e0d5c03303"
},
{
"_id": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
"auth": {
"local": {
"username": "test-partymember-username"
},
"timestamps": {
"created": "2024-10-10T15:57:01.106Z",
"loggedin": "2024-10-30T19:37:01.970Z",
"updated": "2024-10-30T19:38:25.968Z"
}
},
"achievements": {
"ultimateGearSets": {
"healer": false,
"wizard": false,
"rogue": false,
"warrior": false
},
"streak": 0,
"challenges": [],
"perfect": 1,
"quests": {},
"completedTask": true,
"partyUp": true,
"snowball": 1,
"spookySparkles": 1,
"seafoam": 1,
"shinySeed": 1
},
"backer": {},
"contributor": {},
"flags": {
"verifiedUsername": true,
"classSelected": false
},
"items": {
"gear": {
"equipped": {
"armor": "armor_base_0",
"head": "head_base_0",
"shield": "shield_base_0"
},
"costume": {
"armor": "armor_base_0",
"head": "head_base_0",
"shield": "shield_base_0"
},
"owned": {
"headAccessory_special_blackHeadband": true,
"headAccessory_special_blueHeadband": true,
"headAccessory_special_greenHeadband": true,
"headAccessory_special_pinkHeadband": true,
"headAccessory_special_redHeadband": true,
"headAccessory_special_whiteHeadband": true,
"headAccessory_special_yellowHeadband": true,
"eyewear_special_blackTopFrame": true,
"eyewear_special_blueTopFrame": true,
"eyewear_special_greenTopFrame": true,
"eyewear_special_pinkTopFrame": true,
"eyewear_special_redTopFrame": true,
"eyewear_special_whiteTopFrame": true,
"eyewear_special_yellowTopFrame": true,
"eyewear_special_blackHalfMoon": true,
"eyewear_special_blueHalfMoon": true,
"eyewear_special_greenHalfMoon": true,
"eyewear_special_pinkHalfMoon": true,
"eyewear_special_redHalfMoon": true,
"eyewear_special_whiteHalfMoon": true,
"eyewear_special_yellowHalfMoon": true,
"armor_special_bardRobes": true
}
},
"special": {
"snowball": 0,
"spookySparkles": 0,
"shinySeed": 0,
"seafoam": 0,
"valentine": 0,
"valentineReceived": [],
"nye": 0,
"nyeReceived": [],
"greeting": 0,
"greetingReceived": [],
"thankyou": 0,
"thankyouReceived": [],
"birthday": 0,
"birthdayReceived": [],
"congrats": 0,
"congratsReceived": [],
"getwell": 0,
"getwellReceived": [],
"goodluck": 0,
"goodluckReceived": []
},
"lastDrop": {
"count": 0,
"date": "2024-10-30T19:37:01.838Z"
},
"currentPet": "",
"currentMount": "",
"pets": {},
"eggs": {
"BearCub": 1,
"Cactus": 1
},
"hatchingPotions": {
"Skeleton": 1
},
"food": {
"Candy_Red": 1
},
"mounts": {},
"quests": {
"dustbunnies": 1
}
},
"party": {
"quest": {
"progress": {
"up": 0,
"down": 0,
"collectedItems": 0,
"collect": {}
},
"RSVPNeeded": true,
"key": "dustbunnies"
},
"order": "level",
"orderAscending": "ascending",
"_id": "94cd398c-2240-4320-956e-6d345cf2c0de"
},
"preferences": {
"size": "slim",
"hair": {
"color": "red",
"base": 3,
"bangs": 1,
"beard": 0,
"mustache": 0,
"flower": 1
},
"skin": "915533",
"shirt": "blue",
"chair": "none",
"costume": false,
"sleep": false,
"disableClasses": false,
"tasks": {
"groupByChallenge": false,
"confirmScoreNotes": false,
"mirrorGroupTasks": [],
"activeFilter": {
"habit": "all",
"daily": "all",
"todo": "remaining",
"reward": "all"
}
},
"background": "violet"
},
"profile": {
"name": "test-partymember-displayname"
},
"stats": {
"buffs": {
"str": 1,
"int": 1,
"per": 1,
"con": 1,
"stealth": 0,
"streaks": false,
"seafoam": false,
"shinySeed": true,
"snowball": false,
"spookySparkles": false
},
"training": {
"int": 0,
"per": 0,
"str": 0,
"con": 0
},
"hp": 50,
"mp": 24,
"exp": 24,
"gp": 4,
"lvl": 1,
"class": "warrior",
"points": 0,
"str": 0,
"con": 0,
"int": 0,
"per": 0,
"toNextLevel": 25,
"maxHealth": 50,
"maxMP": 32
},
"inbox": {
"optOut": false
},
"loginIncentives": 1,
"id": "ffce870c-3ff3-4fa4-bad1-87612e52b8e7"
}
],
"notifications": [],
"userV": 96,
"appVersion": "5.29.0"
}

View File

@ -2,6 +2,7 @@
"data": {
"api_user": "test-api-user",
"profile": { "name": "test-user" },
"auth": { "local": { "username": "test-username" } },
"stats": {
"buffs": {
"str": 26,
@ -65,6 +66,7 @@
},
"needsCron": true,
"lastCron": "2024-09-21T22:01:55.586Z",
"id": "a380546a-94be-4b8e-8a0b-23e0d5c03303",
"items": {
"gear": {
"equipped": {

View File

@ -10,7 +10,9 @@ import pytest
from homeassistant.components.habitica.const import (
ATTR_CONFIG_ENTRY,
ATTR_DIRECTION,
ATTR_ITEM,
ATTR_SKILL,
ATTR_TARGET,
ATTR_TASK,
DEFAULT_URL,
DOMAIN,
@ -23,12 +25,13 @@ from homeassistant.components.habitica.const import (
SERVICE_SCORE_HABIT,
SERVICE_SCORE_REWARD,
SERVICE_START_QUEST,
SERVICE_TRANSFORMATION,
)
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from .conftest import mock_called_with
from .conftest import load_json_object_fixture, mock_called_with
from tests.common import MockConfigEntry
from tests.test_util.aiohttp import AiohttpClientMocker
@ -62,6 +65,15 @@ async def load_entry(
assert config_entry.state is ConfigEntryState.LOADED
@pytest.fixture(autouse=True)
def uuid_mock() -> Generator[None]:
"""Mock the UUID."""
with patch(
"uuid.uuid4", return_value="5d1935ff-80c8-443c-b2e9-733c66b44745"
) as uuid_mock:
yield uuid_mock.return_value
@pytest.mark.parametrize(
("service_data", "item", "target_id"),
[
@ -546,3 +558,234 @@ async def test_score_task_exceptions(
return_response=True,
blocking=True,
)
@pytest.mark.parametrize(
("service_data", "item", "target_id"),
[
(
{
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
ATTR_ITEM: "spooky_sparkles",
},
"spookySparkles",
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
),
(
{
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
ATTR_ITEM: "shiny_seed",
},
"shinySeed",
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
),
(
{
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
ATTR_ITEM: "seafoam",
},
"seafoam",
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
),
(
{
ATTR_TARGET: "a380546a-94be-4b8e-8a0b-23e0d5c03303",
ATTR_ITEM: "snowball",
},
"snowball",
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
),
(
{
ATTR_TARGET: "test-user",
ATTR_ITEM: "spooky_sparkles",
},
"spookySparkles",
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
),
(
{
ATTR_TARGET: "test-username",
ATTR_ITEM: "spooky_sparkles",
},
"spookySparkles",
"a380546a-94be-4b8e-8a0b-23e0d5c03303",
),
(
{
ATTR_TARGET: "ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
ATTR_ITEM: "spooky_sparkles",
},
"spookySparkles",
"ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
"spookySparkles",
"ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
),
(
{
ATTR_TARGET: "test-partymember-displayname",
ATTR_ITEM: "spooky_sparkles",
},
"spookySparkles",
"ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
),
],
ids=[],
)
async def test_transformation(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
service_data: dict[str, Any],
item: str,
target_id: str,
) -> None:
"""Test Habitica user transformation item action."""
mock_habitica.get(
f"{DEFAULT_URL}/api/v3/groups/party/members",
json=load_json_object_fixture("party_members.json", DOMAIN),
)
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}",
json={"success": True, "data": {}},
)
await hass.services.async_call(
DOMAIN,
SERVICE_TRANSFORMATION,
service_data={
ATTR_CONFIG_ENTRY: config_entry.entry_id,
**service_data,
},
return_response=True,
blocking=True,
)
assert mock_called_with(
mock_habitica,
"post",
f"{DEFAULT_URL}/api/v3/user/class/cast/{item}?targetId={target_id}",
)
@pytest.mark.parametrize(
(
"service_data",
"http_status_members",
"http_status_cast",
"expected_exception",
"expected_exception_msg",
),
[
(
{
ATTR_TARGET: "user-not-found",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.OK,
HTTPStatus.OK,
ServiceValidationError,
"Unable to find target 'user-not-found' in your party",
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.TOO_MANY_REQUESTS,
HTTPStatus.OK,
ServiceValidationError,
RATE_LIMIT_EXCEPTION_MSG,
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.NOT_FOUND,
HTTPStatus.OK,
ServiceValidationError,
"Unable to find target, you are currently not in a party. You can only target yourself",
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.BAD_REQUEST,
HTTPStatus.OK,
HomeAssistantError,
"Unable to connect to Habitica, try again later",
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.OK,
HTTPStatus.TOO_MANY_REQUESTS,
ServiceValidationError,
RATE_LIMIT_EXCEPTION_MSG,
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.OK,
HTTPStatus.UNAUTHORIZED,
ServiceValidationError,
"Unable to use spooky_sparkles, you don't own this item",
),
(
{
ATTR_TARGET: "test-partymember-username",
ATTR_ITEM: "spooky_sparkles",
},
HTTPStatus.OK,
HTTPStatus.BAD_REQUEST,
HomeAssistantError,
"Unable to connect to Habitica, try again later",
),
],
)
@pytest.mark.usefixtures("mock_habitica")
async def test_transformation_exceptions(
hass: HomeAssistant,
config_entry: MockConfigEntry,
mock_habitica: AiohttpClientMocker,
service_data: dict[str, Any],
http_status_members: HTTPStatus,
http_status_cast: HTTPStatus,
expected_exception: Exception,
expected_exception_msg: str,
) -> None:
"""Test Habitica transformation action exceptions."""
mock_habitica.get(
f"{DEFAULT_URL}/api/v3/groups/party/members",
json=load_json_object_fixture("party_members.json", DOMAIN),
status=http_status_members,
)
mock_habitica.post(
f"{DEFAULT_URL}/api/v3/user/class/cast/spookySparkles?targetId=ffce870c-3ff3-4fa4-bad1-87612e52b8e7",
json={"success": True, "data": {}},
status=http_status_cast,
)
with pytest.raises(expected_exception, match=expected_exception_msg):
await hass.services.async_call(
DOMAIN,
SERVICE_TRANSFORMATION,
service_data={
ATTR_CONFIG_ENTRY: config_entry.entry_id,
**service_data,
},
return_response=True,
blocking=True,
)