From d5bda3ac14b0f9773b0dae0dac7a0eff82bbfc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Hjelseth=20H=C3=B8yer?= Date: Thu, 30 Sep 2021 17:11:45 +0200 Subject: [PATCH] Surepetcare reauthorize (#56402) Co-authored-by: J. Nick Koston --- .../components/surepetcare/__init__.py | 10 +- .../components/surepetcare/config_flow.py | 53 +++++- .../surepetcare/test_config_flow.py | 162 +++++++++++++++++- 3 files changed, 210 insertions(+), 15 deletions(-) diff --git a/homeassistant/components/surepetcare/__init__.py b/homeassistant/components/surepetcare/__init__.py index 368a548249d..adf3d07f79e 100644 --- a/homeassistant/components/surepetcare/__init__.py +++ b/homeassistant/components/surepetcare/__init__.py @@ -18,6 +18,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.service import ServiceCall @@ -99,12 +100,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: entry, hass, ) - except SurePetcareAuthenticationError: + except SurePetcareAuthenticationError as error: _LOGGER.error("Unable to connect to surepetcare.io: Wrong credentials!") - return False + raise ConfigEntryAuthFailed from error except SurePetcareError as error: - _LOGGER.error("Unable to connect to surepetcare.io: Wrong %s!", error) - return False + raise ConfigEntryNotReady from error await coordinator.async_config_entry_first_refresh() @@ -188,6 +188,8 @@ class SurePetcareDataCoordinator(DataUpdateCoordinator): """Get the latest data from Sure Petcare.""" try: return await self.surepy.get_entities(refresh=True) + except SurePetcareAuthenticationError as err: + raise ConfigEntryAuthFailed("Invalid username/password") from err except SurePetcareError as err: raise UpdateFailed(f"Unable to fetch data: {err}") from err diff --git a/homeassistant/components/surepetcare/config_flow.py b/homeassistant/components/surepetcare/config_flow.py index bc3589aa4bb..30f20257e8c 100644 --- a/homeassistant/components/surepetcare/config_flow.py +++ b/homeassistant/components/surepetcare/config_flow.py @@ -4,7 +4,7 @@ from __future__ import annotations import logging from typing import Any -from surepy import Surepy +import surepy from surepy.exceptions import SurePetcareAuthenticationError, SurePetcareError import voluptuous as vol @@ -18,7 +18,7 @@ from .const import DOMAIN, SURE_API_TIMEOUT _LOGGER = logging.getLogger(__name__) -STEP_USER_DATA_SCHEMA = vol.Schema( +USER_DATA_SCHEMA = vol.Schema( { vol.Required(CONF_USERNAME): str, vol.Required(CONF_PASSWORD): str, @@ -28,7 +28,7 @@ STEP_USER_DATA_SCHEMA = vol.Schema( async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: """Validate the user input allows us to connect.""" - surepy = Surepy( + surepy_client = surepy.Surepy( data[CONF_USERNAME], data[CONF_PASSWORD], auth_token=None, @@ -36,7 +36,7 @@ async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, session=async_get_clientsession(hass), ) - token = await surepy.sac.get_token() + token = await surepy_client.sac.get_token() return {CONF_TOKEN: token} @@ -46,6 +46,10 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + def __init__(self) -> None: + """Initialize.""" + self._username: str | None = None + async def async_step_import(self, import_info: dict[str, Any] | None) -> FlowResult: """Set the config entry up from yaml.""" return await self.async_step_user(import_info) @@ -55,9 +59,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) -> FlowResult: """Handle the initial step.""" if user_input is None: - return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA - ) + return self.async_show_form(step_id="user", data_schema=USER_DATA_SCHEMA) errors = {} @@ -81,5 +83,40 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): ) return self.async_show_form( - step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + step_id="user", data_schema=USER_DATA_SCHEMA, errors=errors + ) + + async def async_step_reauth(self, config: dict[str, Any]) -> FlowResult: + """Handle configuration by re-auth.""" + self._username = config[CONF_USERNAME] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Dialog that informs the user that reauth is required.""" + errors = {} + if user_input is not None: + user_input[CONF_USERNAME] = self._username + try: + await validate_input(self.hass, user_input) + except SurePetcareAuthenticationError: + errors["base"] = "invalid_auth" + except SurePetcareError: + errors["base"] = "cannot_connect" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + existing_entry = await self.async_set_unique_id( + user_input[CONF_USERNAME].lower() + ) + if existing_entry: + await self.hass.config_entries.async_reload(existing_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, ) diff --git a/tests/components/surepetcare/test_config_flow.py b/tests/components/surepetcare/test_config_flow.py index d397c9b121a..d52dd025148 100644 --- a/tests/components/surepetcare/test_config_flow.py +++ b/tests/components/surepetcare/test_config_flow.py @@ -14,6 +14,11 @@ from homeassistant.data_entry_flow import ( from tests.common import MockConfigEntry +INPUT_DATA = { + "username": "test-username", + "password": "test-password", +} + async def test_form(hass: HomeAssistant, surepetcare: NonCallableMagicMock) -> None: """Test we get the form.""" @@ -54,7 +59,7 @@ async def test_form_invalid_auth(hass: HomeAssistant) -> None: ) with patch( - "surepy.client.SureAPIClient.get_token", + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", side_effect=SurePetcareAuthenticationError, ): result2 = await hass.config_entries.flow.async_configure( @@ -76,7 +81,7 @@ async def test_form_cannot_connect(hass: HomeAssistant) -> None: ) with patch( - "surepy.client.SureAPIClient.get_token", + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", side_effect=SurePetcareError, ): result2 = await hass.config_entries.flow.async_configure( @@ -98,7 +103,7 @@ async def test_form_unknown_error(hass: HomeAssistant) -> None: ) with patch( - "surepy.client.SureAPIClient.get_token", + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", side_effect=Exception, ): result2 = await hass.config_entries.flow.async_configure( @@ -142,3 +147,154 @@ async def test_flow_entry_already_exists( assert result["type"] == RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_reauthentication(hass): + """Test surepetcare reauthentication.""" + old_entry = MockConfigEntry( + domain="surepetcare", + data=INPUT_DATA, + unique_id="test-username", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", + return_value={"token": "token"}, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == "abort" + assert result2["reason"] == "reauth_successful" + + +async def test_reauthentication_failure(hass): + """Test surepetcare reauthentication failure.""" + old_entry = MockConfigEntry( + domain="surepetcare", + data=INPUT_DATA, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", + side_effect=SurePetcareAuthenticationError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "invalid_auth" + + +async def test_reauthentication_cannot_connect(hass): + """Test surepetcare reauthentication failure.""" + old_entry = MockConfigEntry( + domain="surepetcare", + data=INPUT_DATA, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", + side_effect=SurePetcareError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "cannot_connect" + + +async def test_reauthentication_unknown_failure(hass): + """Test surepetcare reauthentication failure.""" + old_entry = MockConfigEntry( + domain="surepetcare", + data=INPUT_DATA, + unique_id="USERID", + ) + old_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": old_entry.unique_id, + "entry_id": old_entry.entry_id, + }, + data=old_entry.data, + ) + + assert result["type"] == "form" + assert result["errors"] == {} + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.surepetcare.config_flow.surepy.client.SureAPIClient.get_token", + side_effect=Exception, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {"password": "test-password"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result["type"] == "form" + assert result2["errors"]["base"] == "unknown"