From 6e15c06aa96ab4965a67667b316517d0d41816af Mon Sep 17 00:00:00 2001 From: Erwin Douna Date: Sat, 22 Jun 2024 11:25:42 +0200 Subject: [PATCH] Melcloud add reconfigure flow (#115999) --- CODEOWNERS | 2 + .../components/melcloud/config_flow.py | 64 ++++++++- .../components/melcloud/manifest.json | 2 +- .../components/melcloud/strings.json | 13 +- tests/components/melcloud/test_config_flow.py | 136 +++++++++++++++++- 5 files changed, 213 insertions(+), 4 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 6999f9e08a0..9b23b5cc83a 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -841,6 +841,8 @@ build.json @home-assistant/supervisor /homeassistant/components/media_source/ @hunterjm /tests/components/media_source/ @hunterjm /homeassistant/components/mediaroom/ @dgomes +/homeassistant/components/melcloud/ @erwindouna +/tests/components/melcloud/ @erwindouna /homeassistant/components/melissa/ @kennedyshead /tests/components/melissa/ @kennedyshead /homeassistant/components/melnor/ @vanstinator diff --git a/homeassistant/components/melcloud/config_flow.py b/homeassistant/components/melcloud/config_flow.py index f071b64988d..c4392535364 100644 --- a/homeassistant/components/melcloud/config_flow.py +++ b/homeassistant/components/melcloud/config_flow.py @@ -25,7 +25,6 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): """Handle a config flow.""" VERSION = 1 - entry: ConfigEntry | None = None async def _create_entry(self, username: str, token: str) -> ConfigFlowResult: @@ -148,3 +147,66 @@ class FlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "cannot_connect" return acquired_token, errors + + async def async_step_reconfigure( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reconfigure_confirm() + + async def async_step_reconfigure_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle a reconfiguration flow initialized by the user.""" + errors: dict[str, str] = {} + acquired_token = None + assert self.entry + + if user_input is not None: + user_input[CONF_USERNAME] = self.entry.data[CONF_USERNAME] + try: + async with asyncio.timeout(10): + acquired_token = await pymelcloud.login( + user_input[CONF_USERNAME], + user_input[CONF_PASSWORD], + async_get_clientsession(self.hass), + ) + except (ClientResponseError, AttributeError) as err: + if ( + isinstance(err, ClientResponseError) + and err.status + in ( + HTTPStatus.UNAUTHORIZED, + HTTPStatus.FORBIDDEN, + ) + or isinstance(err, AttributeError) + and err.name == "get" + ): + errors["base"] = "invalid_auth" + else: + errors["base"] = "cannot_connect" + except ( + TimeoutError, + ClientError, + ): + errors["base"] = "cannot_connect" + + if not errors: + user_input[CONF_TOKEN] = acquired_token + return self.async_update_reload_and_abort( + self.entry, + data={**self.entry.data, **user_input}, + reason="reconfigure_successful", + ) + + return self.async_show_form( + step_id="reconfigure_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + description_placeholders={CONF_USERNAME: self.entry.data[CONF_USERNAME]}, + ) diff --git a/homeassistant/components/melcloud/manifest.json b/homeassistant/components/melcloud/manifest.json index 0122c840373..f61ed412be1 100644 --- a/homeassistant/components/melcloud/manifest.json +++ b/homeassistant/components/melcloud/manifest.json @@ -1,7 +1,7 @@ { "domain": "melcloud", "name": "MELCloud", - "codeowners": [], + "codeowners": ["@erwindouna"], "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/melcloud", "iot_class": "cloud_polling", diff --git a/homeassistant/components/melcloud/strings.json b/homeassistant/components/melcloud/strings.json index 6a98b88e2d3..968f9cf4e50 100644 --- a/homeassistant/components/melcloud/strings.json +++ b/homeassistant/components/melcloud/strings.json @@ -16,6 +16,16 @@ "username": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reconfigure_confirm": { + "title": "Reconfigure your MelCloud", + "description": "Reconfigure the entry to obtain a new token, for your account: `{username}`.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "Enter the (new) password for MelCloud." + } } }, "error": { @@ -25,7 +35,8 @@ }, "abort": { "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", - "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed." + "already_configured": "MELCloud integration already configured for this email. Access token has been refreshed.", + "reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]" } }, "services": { diff --git a/tests/components/melcloud/test_config_flow.py b/tests/components/melcloud/test_config_flow.py index 621838e8c67..c1c6c10ac4c 100644 --- a/tests/components/melcloud/test_config_flow.py +++ b/tests/components/melcloud/test_config_flow.py @@ -9,7 +9,8 @@ import pytest from homeassistant import config_entries from homeassistant.components.melcloud.const import DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_RECONFIGURE +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -305,3 +306,136 @@ async def test_client_errors_reauthentication( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("error", "reason"), + [ + (HTTPStatus.UNAUTHORIZED, "invalid_auth"), + (HTTPStatus.FORBIDDEN, "invalid_auth"), + (HTTPStatus.INTERNAL_SERVER_ERROR, "cannot_connect"), + ], +) +async def test_reconfigure_flow( + hass: HomeAssistant, mock_login, mock_request_info, error, reason +) -> None: + """Test re-configuration flow.""" + mock_login.side_effect = ClientResponseError(mock_request_info(), (), status=error) + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-email@test-domain.com", "token": "test-original-token"}, + unique_id="test-email@test-domain.com", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] is FlowResultType.FORM + + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["errors"]["base"] == reason + assert result["type"] is FlowResultType.FORM + + mock_login.side_effect = None + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry + assert entry.title == "Mock Title" + assert entry.data == { + "username": "test-email@test-domain.com", + "token": "test-token", + "password": "test-password", + } + + +@pytest.mark.parametrize( + ("error", "reason"), + [ + (TimeoutError(), "cannot_connect"), + (AttributeError(name="get"), "invalid_auth"), + ], +) +async def test_form_errors_reconfigure( + hass: HomeAssistant, mock_login, error, reason +) -> None: + """Test we handle cannot connect error.""" + mock_login.side_effect = error + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={"username": "test-email@test-domain.com", "token": "test-original-token"}, + unique_id="test-email@test-domain.com", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_RECONFIGURE, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.FORM + assert result["errors"]["base"] == reason + + mock_login.side_effect = None + with patch( + "homeassistant.components.melcloud.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "test-password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reconfigure_successful" + entry = hass.config_entries.async_get_entry(mock_entry.entry_id) + assert entry + assert entry.title == "Mock Title" + assert entry.data == { + "username": "test-email@test-domain.com", + "token": "test-token", + "password": "test-password", + }