From 89946348df69b607edc920d7e33b471c7169ec1f Mon Sep 17 00:00:00 2001 From: Cyrill Raccaud Date: Tue, 17 Dec 2024 13:54:07 +0100 Subject: [PATCH] Add reconfigure to Cookidoo integration (#133144) * add reconfigure * merge steps * comments --- .../components/cookidoo/config_flow.py | 75 +++++++-- .../components/cookidoo/quality_scale.yaml | 2 +- .../components/cookidoo/strings.json | 6 +- tests/components/cookidoo/test_config_flow.py | 158 ++++++++++++++++++ 4 files changed, 221 insertions(+), 20 deletions(-) diff --git a/homeassistant/components/cookidoo/config_flow.py b/homeassistant/components/cookidoo/config_flow.py index 58e99a70907..120ab162a6c 100644 --- a/homeassistant/components/cookidoo/config_flow.py +++ b/homeassistant/components/cookidoo/config_flow.py @@ -17,7 +17,12 @@ from cookidoo_api import ( ) import voluptuous as vol -from homeassistant.config_entries import ConfigFlow, ConfigFlowResult +from homeassistant.config_entries import ( + SOURCE_RECONFIGURE, + SOURCE_USER, + ConfigFlow, + ConfigFlowResult, +) from homeassistant.const import CONF_COUNTRY, CONF_EMAIL, CONF_LANGUAGE, CONF_PASSWORD from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.selector import ( @@ -58,26 +63,43 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN): user_input: dict[str, Any] - async def async_step_user( - self, user_input: dict[str, Any] | None = None + async def async_step_reconfigure( + self, user_input: dict[str, Any] ) -> ConfigFlowResult: - """Handle the user step.""" + """Perform reconfigure upon an user action.""" + return await self.async_step_user(user_input) + + async def async_step_user( + self, + user_input: dict[str, Any] | None = None, + ) -> ConfigFlowResult: + """Handle the user step as well as serve for reconfiguration.""" errors: dict[str, str] = {} if user_input is not None and not ( errors := await self.validate_input(user_input) ): - self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]}) + if self.source == SOURCE_USER: + self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]}) self.user_input = user_input return await self.async_step_language() await self.generate_country_schema() + suggested_values: dict = {} + if self.source == SOURCE_RECONFIGURE: + reconfigure_entry = self._get_reconfigure_entry() + suggested_values = { + **suggested_values, + **reconfigure_entry.data, + } + if user_input is not None: + suggested_values = {**suggested_values, **user_input} return self.async_show_form( step_id="user", data_schema=self.add_suggested_values_to_schema( data_schema=vol.Schema( {**AUTH_DATA_SCHEMA, **self.COUNTRY_DATA_SCHEMA} ), - suggested_values=user_input, + suggested_values=suggested_values, ), description_placeholders={"cookidoo": "Cookidoo"}, errors=errors, @@ -92,8 +114,18 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN): if language_input is not None and not ( errors := await self.validate_input(self.user_input, language_input) ): - return self.async_create_entry( - title="Cookidoo", data={**self.user_input, **language_input} + if self.source == SOURCE_USER: + return self.async_create_entry( + title="Cookidoo", data={**self.user_input, **language_input} + ) + reconfigure_entry = self._get_reconfigure_entry() + return self.async_update_reload_and_abort( + reconfigure_entry, + data={ + **reconfigure_entry.data, + **self.user_input, + **language_input, + }, ) await self.generate_language_schema() @@ -169,24 +201,35 @@ class CookidooConfigFlow(ConfigFlow, domain=DOMAIN): async def validate_input( self, - user_input: Mapping[str, Any], - language_input: Mapping[str, Any] | None = None, + user_input: dict[str, Any], + language_input: dict[str, Any] | None = None, ) -> dict[str, str]: """Input Helper.""" errors: dict[str, str] = {} + data_input: dict[str, Any] = {} + + if self.source == SOURCE_RECONFIGURE: + reconfigure_entry = self._get_reconfigure_entry() + data_input = {**data_input, **reconfigure_entry.data} + data_input = {**data_input, **user_input} + if language_input: + data_input = {**data_input, **language_input} + else: + data_input[CONF_LANGUAGE] = ( + await get_localization_options(country=data_input[CONF_COUNTRY].lower()) + )[0] # Pick any language to test login + session = async_get_clientsession(self.hass) cookidoo = Cookidoo( session, CookidooConfig( - email=user_input[CONF_EMAIL], - password=user_input[CONF_PASSWORD], + email=data_input[CONF_EMAIL], + password=data_input[CONF_PASSWORD], localization=CookidooLocalizationConfig( - country_code=user_input[CONF_COUNTRY].lower(), - language=language_input[CONF_LANGUAGE] - if language_input - else "de-ch", + country_code=data_input[CONF_COUNTRY].lower(), + language=data_input[CONF_LANGUAGE], ), ), ) diff --git a/homeassistant/components/cookidoo/quality_scale.yaml b/homeassistant/components/cookidoo/quality_scale.yaml index 25069c87c46..95a35829079 100644 --- a/homeassistant/components/cookidoo/quality_scale.yaml +++ b/homeassistant/components/cookidoo/quality_scale.yaml @@ -66,7 +66,7 @@ rules: diagnostics: todo exception-translations: done icon-translations: done - reconfiguration-flow: todo + reconfiguration-flow: done dynamic-devices: status: exempt comment: No dynamic entities available diff --git a/homeassistant/components/cookidoo/strings.json b/homeassistant/components/cookidoo/strings.json index 19f709ddaf8..14344bed13d 100644 --- a/homeassistant/components/cookidoo/strings.json +++ b/homeassistant/components/cookidoo/strings.json @@ -2,7 +2,7 @@ "config": { "step": { "user": { - "title": "Login to {cookidoo}", + "title": "Setup {cookidoo}", "data": { "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]", @@ -11,11 +11,11 @@ "data_description": { "email": "Email used to access your {cookidoo} account.", "password": "Password used to access your {cookidoo} account.", - "country": "Pick your language for the {cookidoo} content." + "country": "Pick your country for the {cookidoo} content." } }, "language": { - "title": "Set language for {cookidoo}", + "title": "Setup {cookidoo}", "data": { "language": "[%key:common::config_flow::data::language%]" }, diff --git a/tests/components/cookidoo/test_config_flow.py b/tests/components/cookidoo/test_config_flow.py index cfdc284dbfe..0057bb3767e 100644 --- a/tests/components/cookidoo/test_config_flow.py +++ b/tests/components/cookidoo/test_config_flow.py @@ -16,6 +16,7 @@ from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType from .conftest import COUNTRY, EMAIL, LANGUAGE, PASSWORD +from .test_init import setup_integration from tests.common import MockConfigEntry @@ -182,6 +183,163 @@ async def test_flow_user_init_data_already_configured( assert result["reason"] == "already_configured" +async def test_flow_reconfigure_success( + hass: HomeAssistant, + cookidoo_config_entry: AsyncMock, + mock_cookidoo_client: AsyncMock, +) -> None: + """Test we get the reconfigure flow and create entry with success.""" + cookidoo_config_entry.add_to_hass(hass) + await setup_integration(hass, cookidoo_config_entry) + + result = await cookidoo_config_entry.start_reconfigure_flow(hass) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["handler"] == "cookidoo" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={**MOCK_DATA_USER_STEP, CONF_COUNTRY: "DE"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "language" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_LANGUAGE: "de-DE"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert cookidoo_config_entry.data == { + **MOCK_DATA_USER_STEP, + CONF_COUNTRY: "DE", + CONF_LANGUAGE: "de-DE", + } + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (CookidooRequestException(), "cannot_connect"), + (CookidooException(), "unknown"), + (IndexError(), "unknown"), + ], +) +async def test_flow_reconfigure_init_data_unknown_error_and_recover_on_step_1( + hass: HomeAssistant, + cookidoo_config_entry: AsyncMock, + mock_cookidoo_client: AsyncMock, + raise_error: Exception, + text_error: str, +) -> None: + """Test unknown errors.""" + mock_cookidoo_client.login.side_effect = raise_error + + cookidoo_config_entry.add_to_hass(hass) + await setup_integration(hass, cookidoo_config_entry) + + result = await cookidoo_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["handler"] == "cookidoo" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={**MOCK_DATA_USER_STEP, CONF_COUNTRY: "DE"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == text_error + + # Recover + mock_cookidoo_client.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={**MOCK_DATA_USER_STEP, CONF_COUNTRY: "DE"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "language" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_LANGUAGE: "de-DE"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert cookidoo_config_entry.data == { + **MOCK_DATA_USER_STEP, + CONF_COUNTRY: "DE", + CONF_LANGUAGE: "de-DE", + } + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("raise_error", "text_error"), + [ + (CookidooRequestException(), "cannot_connect"), + (CookidooException(), "unknown"), + (IndexError(), "unknown"), + ], +) +async def test_flow_reconfigure_init_data_unknown_error_and_recover_on_step_2( + hass: HomeAssistant, + cookidoo_config_entry: AsyncMock, + mock_cookidoo_client: AsyncMock, + raise_error: Exception, + text_error: str, +) -> None: + """Test unknown errors.""" + mock_cookidoo_client.get_additional_items.side_effect = raise_error + + cookidoo_config_entry.add_to_hass(hass) + await setup_integration(hass, cookidoo_config_entry) + + result = await cookidoo_config_entry.start_reconfigure_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "user" + assert result["handler"] == "cookidoo" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={**MOCK_DATA_USER_STEP, CONF_COUNTRY: "DE"}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "language" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_LANGUAGE: "de-DE"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == text_error + + # Recover + mock_cookidoo_client.get_additional_items.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_LANGUAGE: "de-DE"}, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + assert cookidoo_config_entry.data == { + **MOCK_DATA_USER_STEP, + CONF_COUNTRY: "DE", + CONF_LANGUAGE: "de-DE", + } + assert len(hass.config_entries.async_entries()) == 1 + + async def test_flow_reauth( hass: HomeAssistant, mock_cookidoo_client: AsyncMock,