From bafa99fe3e4ab1257491928f55db2a00ca05177c Mon Sep 17 00:00:00 2001 From: Keilin Bickar Date: Sun, 27 Feb 2022 20:47:31 -0500 Subject: [PATCH] Add reauth to SleepIQ (#67321) Co-authored-by: J. Nick Koston --- homeassistant/components/sleepiq/__init__.py | 6 +-- .../components/sleepiq/config_flow.py | 44 ++++++++++++++++++- homeassistant/components/sleepiq/strings.json | 10 ++++- .../components/sleepiq/translations/en.json | 10 ++++- tests/components/sleepiq/conftest.py | 18 +++++--- tests/components/sleepiq/test_config_flow.py | 38 ++++++++++++++-- 6 files changed, 110 insertions(+), 16 deletions(-) diff --git a/homeassistant/components/sleepiq/__init__.py b/homeassistant/components/sleepiq/__init__.py index bac88880cdb..a32e61b972c 100644 --- a/homeassistant/components/sleepiq/__init__.py +++ b/homeassistant/components/sleepiq/__init__.py @@ -12,7 +12,7 @@ import voluptuous as vol from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, 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 import homeassistant.helpers.config_validation as cv from homeassistant.helpers.typing import ConfigType @@ -63,9 +63,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: await gateway.login(email, password) - except SleepIQLoginException: + except SleepIQLoginException as err: _LOGGER.error("Could not authenticate with SleepIQ server") - return False + raise ConfigEntryAuthFailed(err) from err except SleepIQTimeoutException as err: raise ConfigEntryNotReady( str(err) or "Timed out during authentication" diff --git a/homeassistant/components/sleepiq/config_flow.py b/homeassistant/components/sleepiq/config_flow.py index 47e08fdfd5b..49f14eff0b9 100644 --- a/homeassistant/components/sleepiq/config_flow.py +++ b/homeassistant/components/sleepiq/config_flow.py @@ -7,7 +7,7 @@ from typing import Any from asyncsleepiq import AsyncSleepIQ, SleepIQLoginException, SleepIQTimeoutException import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult @@ -18,11 +18,15 @@ from .const import DOMAIN _LOGGER = logging.getLogger(__name__) -class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): +class SleepIQFlowHandler(ConfigFlow, domain=DOMAIN): """Handle a SleepIQ config flow.""" VERSION = 1 + def __init__(self) -> None: + """Initialize the config flow.""" + self._reauth_entry: ConfigEntry | None = None + async def async_step_import(self, import_config: dict[str, Any]) -> FlowResult: """Import a SleepIQ account as a config entry. @@ -75,6 +79,42 @@ class SleepIQFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): last_step=True, ) + async def async_step_reauth(self, user_input: dict[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self._reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm(user_input) + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm reauth.""" + errors: dict[str, str] = {} + assert self._reauth_entry is not None + if user_input is not None: + data = { + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + + if not (error := await try_connection(self.hass, data)): + self.hass.config_entries.async_update_entry( + self._reauth_entry, data=data + ) + await self.hass.config_entries.async_reload(self._reauth_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}), + errors=errors, + description_placeholders={ + CONF_USERNAME: self._reauth_entry.data[CONF_USERNAME], + }, + ) + async def try_connection(hass: HomeAssistant, user_input: dict[str, Any]) -> str | None: """Test if the given credentials can successfully login to SleepIQ.""" diff --git a/homeassistant/components/sleepiq/strings.json b/homeassistant/components/sleepiq/strings.json index 21ceead3d0a..223b8c11c4f 100644 --- a/homeassistant/components/sleepiq/strings.json +++ b/homeassistant/components/sleepiq/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -13,6 +14,13 @@ "password": "[%key:common::config_flow::data::password%]", "username": "[%key:common::config_flow::data::username%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "The SleepIQ integration needs to re-authenticate your account {username}.", + "data": { + "password": "[%key:common::config_flow::data::password%]" + } } } } diff --git a/homeassistant/components/sleepiq/translations/en.json b/homeassistant/components/sleepiq/translations/en.json index 31de29c8690..fcefeace3c8 100644 --- a/homeassistant/components/sleepiq/translations/en.json +++ b/homeassistant/components/sleepiq/translations/en.json @@ -1,13 +1,21 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication" }, "step": { + "reauth_confirm": { + "data": { + "password": "Password" + }, + "description": "The SleepIQ integration needs to re-authenticate your account {username}.", + "title": "Reauthenticate Integration" + }, "user": { "data": { "password": "Password", diff --git a/tests/components/sleepiq/conftest.py b/tests/components/sleepiq/conftest.py index 3669fd5a7fc..9ecc1edd0b6 100644 --- a/tests/components/sleepiq/conftest.py +++ b/tests/components/sleepiq/conftest.py @@ -1,4 +1,6 @@ """Common methods for SleepIQ.""" +from __future__ import annotations + from unittest.mock import create_autospec, patch from asyncsleepiq import SleepIQBed, SleepIQSleeper @@ -20,6 +22,12 @@ SLEEPER_L_NAME_LOWER = SLEEPER_L_NAME.lower().replace(" ", "_") SLEEPER_R_NAME_LOWER = SLEEPER_R_NAME.lower().replace(" ", "_") +SLEEPIQ_CONFIG = { + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", +} + + @pytest.fixture def mock_asyncsleepiq(): """Mock an AsyncSleepIQ object.""" @@ -49,14 +57,14 @@ def mock_asyncsleepiq(): yield client -async def setup_platform(hass: HomeAssistant, platform) -> MockConfigEntry: +async def setup_platform( + hass: HomeAssistant, platform: str | None = None +) -> MockConfigEntry: """Set up the SleepIQ platform.""" mock_entry = MockConfigEntry( domain=DOMAIN, - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - }, + data=SLEEPIQ_CONFIG, + unique_id=SLEEPIQ_CONFIG[CONF_USERNAME].lower(), ) mock_entry.add_to_hass(hass) diff --git a/tests/components/sleepiq/test_config_flow.py b/tests/components/sleepiq/test_config_flow.py index 516a783f302..bb6742821f6 100644 --- a/tests/components/sleepiq/test_config_flow.py +++ b/tests/components/sleepiq/test_config_flow.py @@ -9,10 +9,7 @@ from homeassistant.components.sleepiq.const import DOMAIN from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant -SLEEPIQ_CONFIG = { - CONF_USERNAME: "username", - CONF_PASSWORD: "password", -} +from tests.components.sleepiq.conftest import SLEEPIQ_CONFIG, setup_platform async def test_import(hass: HomeAssistant) -> None: @@ -97,3 +94,36 @@ async def test_success(hass: HomeAssistant) -> None: assert result2["data"][CONF_USERNAME] == SLEEPIQ_CONFIG[CONF_USERNAME] assert result2["data"][CONF_PASSWORD] == SLEEPIQ_CONFIG[CONF_PASSWORD] assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_password(hass): + """Test reauth form.""" + + # set up initially + entry = await setup_platform(hass) + with patch( + "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", + side_effect=SleepIQLoginException, + ): + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.sleepiq.config_flow.AsyncSleepIQ.login", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful"