From b17c36eeff4425ac38f95bca301e464600183245 Mon Sep 17 00:00:00 2001 From: Jan Bouwhuis Date: Sun, 19 Jan 2025 14:26:21 +0100 Subject: [PATCH] Add re-authentication flow to incomfort integration (#135861) --- .../components/incomfort/config_flow.py | 38 ++++++++++++++ .../components/incomfort/strings.json | 10 ++++ tests/components/incomfort/conftest.py | 3 +- .../components/incomfort/test_config_flow.py | 52 +++++++++++++++++++ 4 files changed, 101 insertions(+), 2 deletions(-) diff --git a/homeassistant/components/incomfort/config_flow.py b/homeassistant/components/incomfort/config_flow.py index ffaee2a38a4..bfc43faacf2 100644 --- a/homeassistant/components/incomfort/config_flow.py +++ b/homeassistant/components/incomfort/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any from aiohttp import ClientResponseError @@ -43,6 +44,15 @@ CONFIG_SCHEMA = vol.Schema( } ) +REAUTH_SCHEMA = vol.Schema( + { + vol.Optional(CONF_PASSWORD): TextSelector( + TextSelectorConfig(type=TextSelectorType.PASSWORD) + ), + } +) + + OPTIONS_SCHEMA = vol.Schema( { vol.Optional(CONF_LEGACY_SETPOINT_STATUS, default=False): BooleanSelector( @@ -107,6 +117,34 @@ class InComfortConfigFlow(ConfigFlow, domain=DOMAIN): step_id="user", data_schema=CONFIG_SCHEMA, errors=errors ) + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle re-authentication.""" + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle re-authentication and confirmation.""" + errors: dict[str, str] | None = None + + if user_input: + password: str = user_input[CONF_PASSWORD] + + reauth_entry = self._get_reauth_entry() + errors = await async_try_connect_gateway( + self.hass, reauth_entry.data | {CONF_PASSWORD: password} + ) + if not errors: + return self.async_update_reload_and_abort( + reauth_entry, data_updates={CONF_PASSWORD: password} + ) + + return self.async_show_form( + step_id="reauth_confirm", data_schema=REAUTH_SCHEMA, errors=errors + ) + class InComfortOptionsFlowHandler(OptionsFlow): """Handle InComfort Lan2RF gateway options.""" diff --git a/homeassistant/components/incomfort/strings.json b/homeassistant/components/incomfort/strings.json index 8687be19bb6..9fd31ae1c6f 100644 --- a/homeassistant/components/incomfort/strings.json +++ b/homeassistant/components/incomfort/strings.json @@ -13,6 +13,15 @@ "username": "The username to log into the gateway. This is `admin` in most cases.", "password": "The password to log into the gateway, is printed at the bottom of the Lan2RF Gateway or is `intergas` for some older devices." } + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "Correct the gateway password." + }, + "description": "Re-authenticate to the gateway." } }, "abort": { @@ -21,6 +30,7 @@ "no_heaters": "No heaters found.", "not_found": "No Lan2RF gateway found.", "timeout_error": "Time out when connection to Lan2RF gateway.", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "Unknown error when connection to Lan2RF gateway." }, "error": { diff --git a/tests/components/incomfort/conftest.py b/tests/components/incomfort/conftest.py index a450b7e26d3..3829c42d07f 100644 --- a/tests/components/incomfort/conftest.py +++ b/tests/components/incomfort/conftest.py @@ -8,7 +8,6 @@ from incomfortclient import DisplayCode import pytest from homeassistant.components.incomfort.const import DOMAIN -from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant from tests.common import MockConfigEntry @@ -81,7 +80,7 @@ def mock_config_entry( hass: HomeAssistant, mock_entry_data: dict[str, Any], mock_entry_options: dict[str, Any], -) -> ConfigEntry: +) -> MockConfigEntry: """Mock a config entry setup for incomfort integration.""" entry = MockConfigEntry( domain=DOMAIN, data=mock_entry_data, options=mock_entry_options diff --git a/tests/components/incomfort/test_config_flow.py b/tests/components/incomfort/test_config_flow.py index ab24728874c..0c5ef2f31b1 100644 --- a/tests/components/incomfort/test_config_flow.py +++ b/tests/components/incomfort/test_config_flow.py @@ -116,6 +116,58 @@ async def test_form_validation( assert "errors" not in result +async def test_reauth_flow_success( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the re-authentication flow succeeds.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "new-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_reauth_flow_failure( + hass: HomeAssistant, + mock_incomfort: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test the re-authentication flow fails.""" + await hass.config_entries.async_setup(mock_config_entry.entry_id) + + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch.object( + mock_incomfort(), + "heaters", + side_effect=IncomfortError(ClientResponseError(None, None, status=401)), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "incorrect-password"}, + ) + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {CONF_PASSWORD: "auth_error"} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "new-password"}, + ) + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + @pytest.mark.parametrize( ("user_input", "legacy_setpoint_status"), [