From 4f526a92120f2d4430807c01517d315e0f0dec5e Mon Sep 17 00:00:00 2001 From: Robert Hillis Date: Thu, 25 Aug 2022 09:24:09 -0400 Subject: [PATCH] Add reauth flow to Skybell (#75682) Co-authored-by: J. Nick Koston --- homeassistant/components/skybell/__init__.py | 6 +- .../components/skybell/config_flow.py | 34 +++++++ homeassistant/components/skybell/strings.json | 7 ++ .../components/skybell/translations/en.json | 7 ++ tests/components/skybell/test_config_flow.py | 94 ++++++++++++++++--- 5 files changed, 132 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/skybell/__init__.py b/homeassistant/components/skybell/__init__.py index 1e272dba27f..d986707dfe7 100644 --- a/homeassistant/components/skybell/__init__.py +++ b/homeassistant/components/skybell/__init__.py @@ -11,7 +11,7 @@ from homeassistant.components.repairs.models import IssueSeverity from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, 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.typing import ConfigType @@ -59,8 +59,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) try: devices = await api.async_initialize() - except SkybellAuthenticationException: - return False + except SkybellAuthenticationException as ex: + raise ConfigEntryAuthFailed from ex except SkybellException as ex: raise ConfigEntryNotReady(f"Unable to connect to Skybell service: {ex}") from ex diff --git a/homeassistant/components/skybell/config_flow.py b/homeassistant/components/skybell/config_flow.py index 908eab4c46d..5e63ae4f929 100644 --- a/homeassistant/components/skybell/config_flow.py +++ b/homeassistant/components/skybell/config_flow.py @@ -1,6 +1,7 @@ """Config flow for Skybell integration.""" from __future__ import annotations +from collections.abc import Mapping from typing import Any from aioskybell import Skybell, exceptions @@ -17,6 +18,39 @@ from .const import DOMAIN class SkybellFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for Skybell.""" + reauth_email: str + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle a reauthorization flow request.""" + self.reauth_email = entry_data[CONF_EMAIL] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, str] | None = None + ) -> FlowResult: + """Handle user's reauth credentials.""" + errors = {} + if user_input: + password = user_input[CONF_PASSWORD] + entry_id = self.context["entry_id"] + if entry := self.hass.config_entries.async_get_entry(entry_id): + _, error = await self._async_validate_input(self.reauth_email, password) + if error is None: + self.hass.config_entries.async_update_entry( + entry, + data=entry.data | user_input, + ) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") + + errors["base"] = error + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema({vol.Required(CONF_PASSWORD): str}), + description_placeholders={CONF_EMAIL: self.reauth_email}, + errors=errors, + ) + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: diff --git a/homeassistant/components/skybell/strings.json b/homeassistant/components/skybell/strings.json index 949223250df..f9122a1e100 100644 --- a/homeassistant/components/skybell/strings.json +++ b/homeassistant/components/skybell/strings.json @@ -6,6 +6,13 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "description": "Please update your password for {email}", + "title": "[%key:common::config_flow::title::reauth%]", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { diff --git a/homeassistant/components/skybell/translations/en.json b/homeassistant/components/skybell/translations/en.json index 767f6bfe64e..0a7be932056 100644 --- a/homeassistant/components/skybell/translations/en.json +++ b/homeassistant/components/skybell/translations/en.json @@ -15,6 +15,13 @@ "email": "Email", "password": "Password" } + }, + "reauth_confirm": { + "description": "Please update your password for {email}", + "title": "Reauthenticate Integration", + "data": { + "password": "Password" + } } } }, diff --git a/tests/components/skybell/test_config_flow.py b/tests/components/skybell/test_config_flow.py index 21ead201b54..9b7afd12c93 100644 --- a/tests/components/skybell/test_config_flow.py +++ b/tests/components/skybell/test_config_flow.py @@ -3,12 +3,20 @@ from unittest.mock import patch from aioskybell import exceptions +from homeassistant import config_entries from homeassistant.components.skybell.const import DOMAIN from homeassistant.config_entries import SOURCE_USER +from homeassistant.const import CONF_PASSWORD, CONF_SOURCE from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType -from . import CONF_CONFIG_FLOW, _patch_skybell, _patch_skybell_devices +from . import ( + CONF_CONFIG_FLOW, + PASSWORD, + USER_ID, + _patch_skybell, + _patch_skybell_devices, +) from tests.common import MockConfigEntry @@ -20,16 +28,9 @@ def _patch_setup_entry() -> None: ) -def _patch_setup() -> None: - return patch( - "homeassistant.components.skybell.async_setup", - return_value=True, - ) - - async def test_flow_user(hass: HomeAssistant) -> None: """Test that the user step works.""" - with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(), _patch_setup(): + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER} ) @@ -45,6 +46,7 @@ async def test_flow_user(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.CREATE_ENTRY assert result["title"] == "user" assert result["data"] == CONF_CONFIG_FLOW + assert result["result"].unique_id == USER_ID async def test_flow_user_already_configured(hass: HomeAssistant) -> None: @@ -55,10 +57,10 @@ async def test_flow_user_already_configured(hass: HomeAssistant) -> None: ) entry.add_to_hass(hass) - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW - ) + with _patch_skybell(), _patch_skybell_devices(): + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": SOURCE_USER}, data=CONF_CONFIG_FLOW + ) assert result["type"] == FlowResultType.ABORT assert result["reason"] == "already_configured" @@ -99,3 +101,69 @@ async def test_flow_user_unknown_error(hass: HomeAssistant) -> None: assert result["type"] == FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {"base": "unknown"} + + +async def test_step_reauth(hass: HomeAssistant) -> None: + """Test the reauth flow.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=CONF_CONFIG_FLOW) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +async def test_step_reauth_failed(hass: HomeAssistant) -> None: + """Test the reauth flow fails and recovers.""" + entry = MockConfigEntry(domain=DOMAIN, unique_id=USER_ID, data=CONF_CONFIG_FLOW) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + CONF_SOURCE: config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + with patch("homeassistant.components.skybell.Skybell.async_login") as skybell_mock: + skybell_mock.side_effect = exceptions.SkybellAuthenticationException(hass) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {"base": "invalid_auth"} + + with _patch_skybell(), _patch_skybell_devices(), _patch_setup_entry(): + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: PASSWORD}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful"