From f8de4c3931fbc3c4d9cfde3b9297b23a98571648 Mon Sep 17 00:00:00 2001 From: On Freund Date: Tue, 1 Nov 2022 00:01:22 +0200 Subject: [PATCH] Reauth flow for Risco cloud (#81264) * Risco reauth flow * Address code review comments * Remove redundant log --- homeassistant/components/risco/__init__.py | 9 ++- homeassistant/components/risco/config_flow.py | 26 ++++++- tests/components/risco/conftest.py | 5 +- tests/components/risco/test_config_flow.py | 70 +++++++++++++++++++ 4 files changed, 101 insertions(+), 9 deletions(-) diff --git a/homeassistant/components/risco/__init__.py b/homeassistant/components/risco/__init__.py index 0e631cc4a93..a9a462bf916 100644 --- a/homeassistant/components/risco/__init__.py +++ b/homeassistant/components/risco/__init__.py @@ -26,7 +26,7 @@ from homeassistant.const import ( Platform, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.storage import Store @@ -127,10 +127,9 @@ async def _async_setup_cloud_entry(hass: HomeAssistant, entry: ConfigEntry) -> b try: await risco.login(async_get_clientsession(hass)) except CannotConnectError as error: - raise ConfigEntryNotReady() from error - except UnauthorizedError: - _LOGGER.exception("Failed to login to Risco cloud") - return False + raise ConfigEntryNotReady from error + except UnauthorizedError as error: + raise ConfigEntryAuthFailed from error scan_interval = entry.options.get(CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL) coordinator = RiscoDataUpdateCoordinator(hass, risco, scan_interval) diff --git a/homeassistant/components/risco/config_flow.py b/homeassistant/components/risco/config_flow.py index 5e1cdb75b5a..91e12a2548a 100644 --- a/homeassistant/components/risco/config_flow.py +++ b/homeassistant/components/risco/config_flow.py @@ -3,6 +3,7 @@ from __future__ import annotations from collections.abc import Mapping import logging +from typing import Any from pyrisco import CannotConnectError, RiscoCloud, RiscoLocal, UnauthorizedError import voluptuous as vol @@ -21,6 +22,7 @@ from homeassistant.const import ( STATE_ALARM_ARMED_HOME, STATE_ALARM_ARMED_NIGHT, ) +from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -93,6 +95,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Init the config flow.""" + self._reauth_entry: config_entries.ConfigEntry | None = None + @staticmethod @core.callback def async_get_options_flow( @@ -112,8 +118,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Configure a cloud based alarm.""" errors = {} if user_input is not None: - await self.async_set_unique_id(user_input[CONF_USERNAME]) - self._abort_if_unique_id_configured() + if not self._reauth_entry: + await self.async_set_unique_id(user_input[CONF_USERNAME]) + self._abort_if_unique_id_configured() try: info = await validate_cloud_input(self.hass, user_input) @@ -125,12 +132,25 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" else: - return self.async_create_entry(title=info["title"], data=user_input) + if not self._reauth_entry: + return self.async_create_entry(title=info["title"], data=user_input) + self.hass.config_entries.async_update_entry( + self._reauth_entry, + data=user_input, + unique_id=user_input[CONF_USERNAME], + ) + await 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="cloud", data_schema=CLOUD_SCHEMA, errors=errors ) + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._reauth_entry = await self.async_set_unique_id(entry_data[CONF_USERNAME]) + return await self.async_step_cloud() + async def async_step_local(self, user_input=None): """Configure a local based alarm.""" errors = {} diff --git a/tests/components/risco/conftest.py b/tests/components/risco/conftest.py index 006e57b9ae5..b22768a1cd0 100644 --- a/tests/components/risco/conftest.py +++ b/tests/components/risco/conftest.py @@ -70,7 +70,10 @@ def events(): def cloud_config_entry(hass, options): """Fixture for a cloud config entry.""" config_entry = MockConfigEntry( - domain=DOMAIN, data=TEST_CLOUD_CONFIG, options=options + domain=DOMAIN, + data=TEST_CLOUD_CONFIG, + options=options, + unique_id=TEST_CLOUD_CONFIG[CONF_USERNAME], ) config_entry.add_to_hass(hass) return config_entry diff --git a/tests/components/risco/test_config_flow.py b/tests/components/risco/test_config_flow.py index 396aad8015d..0c71ba9efdc 100644 --- a/tests/components/risco/test_config_flow.py +++ b/tests/components/risco/test_config_flow.py @@ -10,6 +10,7 @@ from homeassistant.components.risco.config_flow import ( UnauthorizedError, ) from homeassistant.components.risco.const import DOMAIN +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -142,6 +143,75 @@ async def test_form_cloud_already_exists(hass): assert result3["reason"] == "already_configured" +async def test_form_reauth(hass, cloud_config_entry): + """Test reauthenticate.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=cloud_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**TEST_CLOUD_DATA, CONF_PASSWORD: "new_password"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert cloud_config_entry.data[CONF_PASSWORD] == "new_password" + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_reauth_with_new_username(hass, cloud_config_entry): + """Test reauthenticate with new username.""" + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH}, + data=cloud_config_entry.data, + ) + assert result["type"] == "form" + assert result["errors"] == {} + + with patch( + "homeassistant.components.risco.config_flow.RiscoCloud.login", + return_value=True, + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.site_name", + new_callable=PropertyMock(return_value=TEST_SITE_NAME), + ), patch( + "homeassistant.components.risco.config_flow.RiscoCloud.close" + ), patch( + "homeassistant.components.risco.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], {**TEST_CLOUD_DATA, CONF_USERNAME: "new_user"} + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + assert cloud_config_entry.data[CONF_USERNAME] == "new_user" + assert cloud_config_entry.unique_id == "new_user" + assert len(mock_setup_entry.mock_calls) == 1 + + async def test_local_form(hass): """Test we get the local form.""" result = await hass.config_entries.flow.async_init(