From 8d50be379c29bc6c4687a36854c6e45a9bbcb79d Mon Sep 17 00:00:00 2001 From: G Johansson Date: Mon, 25 Sep 2023 08:59:15 +0200 Subject: [PATCH] Create repairs in Workday if country or province is wrong (#98753) * Repairs workday * fix if not province exist * Tests repairs * Add tests * Finalize tests * Fix feedback * simplify * Less translations --- homeassistant/components/workday/__init__.py | 25 +- homeassistant/components/workday/repairs.py | 124 ++++++ homeassistant/components/workday/strings.json | 43 ++ tests/components/workday/test_repairs.py | 399 ++++++++++++++++++ 4 files changed, 590 insertions(+), 1 deletion(-) create mode 100644 homeassistant/components/workday/repairs.py create mode 100644 tests/components/workday/test_repairs.py diff --git a/homeassistant/components/workday/__init__.py b/homeassistant/components/workday/__init__.py index c3bf7f2efd5..558e0aa9ecf 100644 --- a/homeassistant/components/workday/__init__.py +++ b/homeassistant/components/workday/__init__.py @@ -6,8 +6,9 @@ from holidays import list_supported_countries from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from homeassistant.exceptions import ConfigEntryError +from homeassistant.helpers.issue_registry import IssueSeverity, async_create_issue -from .const import CONF_COUNTRY, CONF_PROVINCE, PLATFORMS +from .const import CONF_COUNTRY, CONF_PROVINCE, DOMAIN, PLATFORMS async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: @@ -17,9 +18,31 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: province: str | None = entry.options.get(CONF_PROVINCE) if country and country not in list_supported_countries(): + async_create_issue( + hass, + DOMAIN, + "bad_country", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.ERROR, + translation_key="bad_country", + translation_placeholders={"title": entry.title}, + data={"entry_id": entry.entry_id, "country": None}, + ) raise ConfigEntryError(f"Selected country {country} is not valid") if country and province and province not in list_supported_countries()[country]: + async_create_issue( + hass, + DOMAIN, + "bad_province", + is_fixable=True, + is_persistent=True, + severity=IssueSeverity.ERROR, + translation_key="bad_province", + translation_placeholders={CONF_COUNTRY: country, "title": entry.title}, + data={"entry_id": entry.entry_id, "country": country}, + ) raise ConfigEntryError( f"Selected province {province} for country {country} is not valid" ) diff --git a/homeassistant/components/workday/repairs.py b/homeassistant/components/workday/repairs.py new file mode 100644 index 00000000000..ff643ecc2cb --- /dev/null +++ b/homeassistant/components/workday/repairs.py @@ -0,0 +1,124 @@ +"""Repairs platform for the Workday integration.""" + +from __future__ import annotations + +from typing import Any, cast + +from holidays import list_supported_countries +import voluptuous as vol + +from homeassistant import data_entry_flow +from homeassistant.components.repairs import ConfirmRepairFlow, RepairsFlow +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.selector import ( + SelectSelector, + SelectSelectorConfig, + SelectSelectorMode, +) + +from .config_flow import NONE_SENTINEL +from .const import CONF_PROVINCE + + +class CountryFixFlow(RepairsFlow): + """Handler for an issue fixing flow.""" + + def __init__(self, entry: ConfigEntry, country: str | None) -> None: + """Create flow.""" + self.entry = entry + self.country: str | None = country + super().__init__() + + async def async_step_init( + self, user_input: dict[str, str] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the first step of a fix flow.""" + if self.country: + return await self.async_step_province() + return await self.async_step_country() + + async def async_step_country( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the country step of a fix flow.""" + if user_input is not None: + all_countries = list_supported_countries() + if not all_countries[user_input[CONF_COUNTRY]]: + options = dict(self.entry.options) + new_options = {**options, **user_input, CONF_PROVINCE: None} + self.hass.config_entries.async_update_entry( + self.entry, options=new_options + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_create_entry(data={}) + self.country = user_input[CONF_COUNTRY] + return await self.async_step_province() + + return self.async_show_form( + step_id="country", + data_schema=vol.Schema( + { + vol.Required(CONF_COUNTRY): SelectSelector( + SelectSelectorConfig( + options=sorted(list_supported_countries()), + mode=SelectSelectorMode.DROPDOWN, + ) + ) + } + ), + description_placeholders={"title": self.entry.title}, + ) + + async def async_step_province( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle the province step of a fix flow.""" + if user_input and user_input.get(CONF_PROVINCE): + if user_input.get(CONF_PROVINCE, NONE_SENTINEL) == NONE_SENTINEL: + user_input[CONF_PROVINCE] = None + options = dict(self.entry.options) + new_options = {**options, **user_input, CONF_COUNTRY: self.country} + self.hass.config_entries.async_update_entry(self.entry, options=new_options) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_create_entry(data={}) + + assert self.country + country_provinces = list_supported_countries()[self.country] + return self.async_show_form( + step_id="province", + data_schema=vol.Schema( + { + vol.Optional(CONF_PROVINCE, default=NONE_SENTINEL): SelectSelector( + SelectSelectorConfig( + options=[NONE_SENTINEL, *country_provinces], + mode=SelectSelectorMode.DROPDOWN, + translation_key=CONF_PROVINCE, + ) + ), + } + ), + description_placeholders={ + CONF_COUNTRY: self.country, + "title": self.entry.title, + }, + ) + + +async def async_create_fix_flow( + hass: HomeAssistant, + issue_id: str, + data: dict[str, Any] | None, +) -> RepairsFlow: + """Create flow.""" + entry = None + if data and (entry_id := data.get("entry_id")): + entry_id = cast(str, entry_id) + entry = hass.config_entries.async_get_entry(entry_id) + + if data and entry: + # Country or province does not exist + return CountryFixFlow(entry, data.get("country")) + + return ConfirmRepairFlow() diff --git a/homeassistant/components/workday/strings.json b/homeassistant/components/workday/strings.json index b4bad4796bc..718f99d7c8a 100644 --- a/homeassistant/components/workday/strings.json +++ b/homeassistant/components/workday/strings.json @@ -88,5 +88,48 @@ "holiday": "Holidays" } } + }, + "issues": { + "bad_country": { + "title": "Configured Country for {title} does not exist", + "fix_flow": { + "step": { + "country": { + "title": "Select country for {title}", + "description": "Select a country to use for your Workday sensor.", + "data": { + "country": "[%key:component::workday::config::step::user::data::country%]" + } + }, + "province": { + "title": "Select province for {title}", + "description": "Select a province in country {country} to use for your Workday sensor.", + "data": { + "province": "[%key:component::workday::config::step::options::data::province%]" + }, + "data_description": { + "province": "State, Territory, Province, Region of Country" + } + } + } + } + }, + "bad_province": { + "title": "Configured province in country {country} for {title} does not exist", + "fix_flow": { + "step": { + "province": { + "title": "[%key:component::workday::issues::bad_country::fix_flow::step::province::title%]", + "description": "[%key:component::workday::issues::bad_country::fix_flow::step::province::description%]", + "data": { + "province": "[%key:component::workday::config::step::options::data::province%]" + }, + "data_description": { + "province": "[%key:component::workday::issues::bad_country::fix_flow::step::province::data_description::province%]" + } + } + } + } + } } } diff --git a/tests/components/workday/test_repairs.py b/tests/components/workday/test_repairs.py new file mode 100644 index 00000000000..38b2142dfb7 --- /dev/null +++ b/tests/components/workday/test_repairs.py @@ -0,0 +1,399 @@ +"""Test repairs for unifiprotect.""" +from __future__ import annotations + +from http import HTTPStatus + +from homeassistant.components.repairs.websocket_api import ( + RepairsFlowIndexView, + RepairsFlowResourceView, +) +from homeassistant.components.workday.const import DOMAIN +from homeassistant.const import CONF_COUNTRY +from homeassistant.core import HomeAssistant +from homeassistant.helpers.issue_registry import async_create_issue +from homeassistant.setup import async_setup_component + +from . import ( + TEST_CONFIG_INCORRECT_COUNTRY, + TEST_CONFIG_INCORRECT_PROVINCE, + init_integration, +) + +from tests.common import ANY +from tests.typing import ClientSessionGenerator, WebSocketGenerator + + +async def test_bad_country( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test fixing bad country.""" + assert await async_setup_component(hass, "repairs", {}) + entry = await init_integration(hass, TEST_CONFIG_INCORRECT_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert not state + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_country": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "bad_country"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"title": entry.title} + assert data["step_id"] == "country" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"country": "DE"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"province": "HB"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_country": + issue = i + assert not issue + + +async def test_bad_country_none( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test fixing bad country with no province.""" + assert await async_setup_component(hass, "repairs", {}) + entry = await init_integration(hass, TEST_CONFIG_INCORRECT_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert not state + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_country": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "bad_country"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"title": entry.title} + assert data["step_id"] == "country" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"country": "DE"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"province": "none"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_country": + issue = i + assert not issue + + +async def test_bad_country_no_province( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test fixing bad country.""" + assert await async_setup_component(hass, "repairs", {}) + entry = await init_integration(hass, TEST_CONFIG_INCORRECT_COUNTRY) + + state = hass.states.get("binary_sensor.workday_sensor") + assert not state + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_country": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "bad_country"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == {"title": entry.title} + assert data["step_id"] == "country" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"country": "SE"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_country": + issue = i + assert not issue + + +async def test_bad_province( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test fixing bad province.""" + assert await async_setup_component(hass, "repairs", {}) + entry = await init_integration(hass, TEST_CONFIG_INCORRECT_PROVINCE) + + state = hass.states.get("binary_sensor.workday_sensor") + assert not state + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_province": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "bad_province"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == { + CONF_COUNTRY: "DE", + "title": entry.title, + } + assert data["step_id"] == "province" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"province": "BW"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_province": + issue = i + assert not issue + + +async def test_bad_province_none( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test fixing bad province selecting none.""" + assert await async_setup_component(hass, "repairs", {}) + entry = await init_integration(hass, TEST_CONFIG_INCORRECT_PROVINCE) + + state = hass.states.get("binary_sensor.workday_sensor") + assert not state + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + assert len(msg["result"]["issues"]) > 0 + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_province": + issue = i + assert issue is not None + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "bad_province"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["description_placeholders"] == { + CONF_COUNTRY: "DE", + "title": entry.title, + } + assert data["step_id"] == "province" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url, json={"province": "none"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done() + + state = hass.states.get("binary_sensor.workday_sensor") + assert state + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + issue = None + for i in msg["result"]["issues"]: + if i["issue_id"] == "bad_province": + issue = i + assert not issue + + +async def test_other_fixable_issues( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + hass_ws_client: WebSocketGenerator, +) -> None: + """Test fixing bad province selecting none.""" + assert await async_setup_component(hass, "repairs", {}) + await init_integration(hass, TEST_CONFIG_INCORRECT_PROVINCE) + + ws_client = await hass_ws_client(hass) + client = await hass_client() + + await ws_client.send_json({"id": 1, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + + issue = { + "breaks_in_ha_version": "2022.9.0dev0", + "domain": DOMAIN, + "issue_id": "issue_1", + "is_fixable": True, + "learn_more_url": "", + "severity": "error", + "translation_key": "issue_1", + } + async_create_issue( + hass, + issue["domain"], + issue["issue_id"], + breaks_in_ha_version=issue["breaks_in_ha_version"], + is_fixable=issue["is_fixable"], + is_persistent=False, + learn_more_url=None, + severity=issue["severity"], + translation_key=issue["translation_key"], + ) + + await ws_client.send_json({"id": 2, "type": "repairs/list_issues"}) + msg = await ws_client.receive_json() + + assert msg["success"] + results = msg["result"]["issues"] + assert { + "breaks_in_ha_version": "2022.9.0dev0", + "created": ANY, + "dismissed_version": None, + "domain": "workday", + "is_fixable": True, + "issue_domain": None, + "issue_id": "issue_1", + "learn_more_url": None, + "severity": "error", + "translation_key": "issue_1", + "translation_placeholders": None, + "ignored": False, + } in results + + url = RepairsFlowIndexView.url + resp = await client.post(url, json={"handler": DOMAIN, "issue_id": "issue_1"}) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + flow_id = data["flow_id"] + assert data["step_id"] == "confirm" + + url = RepairsFlowResourceView.url.format(flow_id=flow_id) + resp = await client.post(url) + assert resp.status == HTTPStatus.OK + data = await resp.json() + + assert data["type"] == "create_entry" + await hass.async_block_till_done()