From 601fb5ebb509e955459dd67060f0d92bab1a7a14 Mon Sep 17 00:00:00 2001 From: rappenze Date: Mon, 5 Sep 2022 14:55:12 +0200 Subject: [PATCH] Add reauth flow to fibaro (#74300) --- homeassistant/components/fibaro/__init__.py | 10 +- .../components/fibaro/config_flow.py | 45 +++++ homeassistant/components/fibaro/strings.json | 10 +- tests/components/fibaro/test_config_flow.py | 162 ++++++++++++++++-- 4 files changed, 212 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/fibaro/__init__.py b/homeassistant/components/fibaro/__init__.py index 7ab83d796f2..9c2d252d77f 100644 --- a/homeassistant/components/fibaro/__init__.py +++ b/homeassistant/components/fibaro/__init__.py @@ -27,7 +27,11 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import device_registry as dr import homeassistant.helpers.config_validation as cv from homeassistant.helpers.entity import DeviceInfo, Entity @@ -497,8 +501,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: raise ConfigEntryNotReady( f"Could not connect to controller at {entry.data[CONF_URL]}" ) from connect_ex - except FibaroAuthFailed: - return False + except FibaroAuthFailed as auth_ex: + raise ConfigEntryAuthFailed from auth_ex data: dict[str, Any] = {} hass.data.setdefault(DOMAIN, {})[entry.entry_id] = data diff --git a/homeassistant/components/fibaro/config_flow.py b/homeassistant/components/fibaro/config_flow.py index fd53bd5b94f..7a6d7422520 100644 --- a/homeassistant/components/fibaro/config_flow.py +++ b/homeassistant/components/fibaro/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Fibaro integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -58,6 +59,10 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -83,3 +88,43 @@ class FibaroConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, import_config: ConfigType | None) -> FlowResult: """Import a config entry.""" return await self.async_step_user(import_config) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle reauthentication.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle a flow initiated by reauthentication.""" + errors = {} + + assert self._reauth_entry + if user_input is not None: + new_data = self._reauth_entry.data | user_input + try: + await _validate_input(self.hass, new_data) + except FibaroConnectFailed: + errors["base"] = "cannot_connect" + except FibaroAuthFailed: + errors["base"] = "invalid_auth" + else: + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=new_data + ) + self.hass.async_create_task( + self.hass.config_entries.async_reload(self._reauth_entry.entry_id) + ) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME] + }, + ) diff --git a/homeassistant/components/fibaro/strings.json b/homeassistant/components/fibaro/strings.json index 99c25c9f6e0..de875176cdb 100644 --- a/homeassistant/components/fibaro/strings.json +++ b/homeassistant/components/fibaro/strings.json @@ -8,6 +8,13 @@ "password": "[%key:common::config_flow::data::password%]", "import_plugins": "Import entities from fibaro plugins?" } + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please update your password for {username}" } }, "error": { @@ -16,7 +23,8 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" } } } diff --git a/tests/components/fibaro/test_config_flow.py b/tests/components/fibaro/test_config_flow.py index 14f28257588..f68bb5fe4ca 100644 --- a/tests/components/fibaro/test_config_flow.py +++ b/tests/components/fibaro/test_config_flow.py @@ -9,6 +9,8 @@ from homeassistant.components.fibaro import DOMAIN from homeassistant.components.fibaro.const import CONF_IMPORT_PLUGINS from homeassistant.const import CONF_PASSWORD, CONF_URL, CONF_USERNAME +from tests.common import MockConfigEntry + TEST_SERIALNUMBER = "HC2-111111" TEST_NAME = "my_fibaro_home_center" TEST_URL = "http://192.168.1.1/api/" @@ -31,20 +33,27 @@ def fibaro_client_fixture(): client_mock = Mock() client_mock.base_url.return_value = TEST_URL - with patch("fiblary3.client.v4.client.Client.__init__", return_value=None,), patch( - "fiblary3.client.v4.client.Client.info", + with patch( + "homeassistant.components.fibaro.FibaroClientV4.__init__", + return_value=None, + ), patch( + "homeassistant.components.fibaro.FibaroClientV4.info", info_mock, create=True, - ), patch("fiblary3.client.v4.client.Client.rooms", array_mock, create=True,), patch( - "fiblary3.client.v4.client.Client.devices", + ), patch( + "homeassistant.components.fibaro.FibaroClientV4.rooms", array_mock, create=True, ), patch( - "fiblary3.client.v4.client.Client.scenes", + "homeassistant.components.fibaro.FibaroClientV4.devices", array_mock, create=True, ), patch( - "fiblary3.client.v4.client.Client.client", + "homeassistant.components.fibaro.FibaroClientV4.scenes", + array_mock, + create=True, + ), patch( + "homeassistant.components.fibaro.FibaroClientV4.client", client_mock, create=True, ): @@ -64,7 +73,7 @@ async def test_config_flow_user_initiated_success(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=True) with patch( - "fiblary3.client.v4.client.Client.login", login_mock, create=True + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True ), patch( "homeassistant.components.fibaro.async_setup_entry", return_value=True, @@ -100,7 +109,9 @@ async def test_config_flow_user_initiated_connect_failure(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=False) - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -127,7 +138,9 @@ async def test_config_flow_user_initiated_auth_failure(hass): login_mock = Mock() login_mock.get.side_effect = HTTPException(details="Forbidden") - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -154,7 +167,9 @@ async def test_config_flow_user_initiated_unknown_failure_1(hass): login_mock = Mock() login_mock.get.side_effect = HTTPException(details="Any") - with patch("fiblary3.client.v4.client.Client.login", login_mock, create=True): + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): result = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -198,7 +213,7 @@ async def test_config_flow_import(hass): login_mock = Mock() login_mock.get.return_value = Mock(status=True) with patch( - "fiblary3.client.v4.client.Client.login", login_mock, create=True + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True ), patch( "homeassistant.components.fibaro.async_setup_entry", return_value=True, @@ -222,3 +237,128 @@ async def test_config_flow_import(hass): CONF_PASSWORD: TEST_PASSWORD, CONF_IMPORT_PLUGINS: False, } + + +async def test_reauth_success(hass): + """Successful reauth flow initialized by the user.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + entry_id=TEST_SERIALNUMBER, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=True) + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ), patch( + "homeassistant.components.fibaro.async_setup_entry", + return_value=True, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "other_fake_password"}, + ) + + assert result["type"] == "abort" + assert result["reason"] == "reauth_successful" + + +async def test_reauth_connect_failure(hass): + """Successful reauth flow initialized by the user.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + entry_id=TEST_SERIALNUMBER, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.return_value = Mock(status=False) + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "other_fake_password"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "cannot_connect"} + + +async def test_reauth_auth_failure(hass): + """Successful reauth flow initialized by the user.""" + mock_config = MockConfigEntry( + domain=DOMAIN, + entry_id=TEST_SERIALNUMBER, + data={ + CONF_URL: TEST_URL, + CONF_USERNAME: TEST_USERNAME, + CONF_PASSWORD: TEST_PASSWORD, + CONF_IMPORT_PLUGINS: False, + }, + ) + mock_config.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config.entry_id, + }, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {} + + login_mock = Mock() + login_mock.get.side_effect = HTTPException(details="Forbidden") + with patch( + "homeassistant.components.fibaro.FibaroClientV4.login", login_mock, create=True + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: "other_fake_password"}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"}