diff --git a/homeassistant/components/sensibo/config_flow.py b/homeassistant/components/sensibo/config_flow.py index dca5ea62fe6..b8cca2e6fb8 100644 --- a/homeassistant/components/sensibo/config_flow.py +++ b/homeassistant/components/sensibo/config_flow.py @@ -1,6 +1,8 @@ """Adds config flow for Sensibo integration.""" from __future__ import annotations +from typing import Any + from pysensibo.exceptions import AuthenticationError import voluptuous as vol @@ -24,6 +26,55 @@ class SensiboConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 2 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle re-authentication with Sensibo.""" + + self.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: + """Confirm re-authentication with Sensibo.""" + errors: dict[str, str] = {} + + if user_input: + api_key = user_input[CONF_API_KEY] + try: + username = await async_validate_api(self.hass, api_key) + except AuthenticationError: + errors["base"] = "invalid_auth" + except ConnectionError: + errors["base"] = "cannot_connect" + except NoDevicesError: + errors["base"] = "no_devices" + except NoUsernameError: + errors["base"] = "no_username" + else: + assert self.entry is not None + + if username == self.entry.unique_id: + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_API_KEY: api_key, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + errors["base"] = "incorrect_api_key" + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=DATA_SCHEMA, + errors=errors, + ) + async def async_step_import(self, config: dict) -> FlowResult: """Import a configuration from config.yaml.""" diff --git a/homeassistant/components/sensibo/coordinator.py b/homeassistant/components/sensibo/coordinator.py index ef0475640b5..926232ef159 100644 --- a/homeassistant/components/sensibo/coordinator.py +++ b/homeassistant/components/sensibo/coordinator.py @@ -10,6 +10,7 @@ from pysensibo.exceptions import AuthenticationError, SensiboError from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_API_KEY from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -41,7 +42,9 @@ class SensiboDataUpdateCoordinator(DataUpdateCoordinator): data = await self.client.async_get_devices() for dev in data["result"]: devices.append(dev) - except (AuthenticationError, SensiboError) as error: + except AuthenticationError as error: + raise ConfigEntryAuthFailed from error + except SensiboError as error: raise UpdateFailed from error device_data: dict[str, dict[str, Any]] = {} diff --git a/homeassistant/components/sensibo/strings.json b/homeassistant/components/sensibo/strings.json index 9b035bc7f05..2b7b351e27f 100644 --- a/homeassistant/components/sensibo/strings.json +++ b/homeassistant/components/sensibo/strings.json @@ -1,19 +1,25 @@ { "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%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", "no_devices": "No devices discovered", - "no_username": "Could not get username" + "no_username": "Could not get username", + "incorrect_api_key": "Invalid API key for selected account" }, "step": { "user": { "data": { - "api_key": "[%key:common::config_flow::data::api_key%]", - "name": "[%key:common::config_flow::data::name%]" + "api_key": "[%key:common::config_flow::data::api_key%]" + } + }, + "reauth_confirm": { + "data": { + "api_key": "[%key:common::config_flow::data::api_key%]" } } } diff --git a/homeassistant/components/sensibo/translations/en.json b/homeassistant/components/sensibo/translations/en.json index 102ffa35879..4acdf4862aa 100644 --- a/homeassistant/components/sensibo/translations/en.json +++ b/homeassistant/components/sensibo/translations/en.json @@ -1,19 +1,25 @@ { "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", "no_devices": "No devices discovered", - "no_username": "Could not get username" + "no_username": "Could not get username", + "incorrect_api_key": "Invalid API key for selected account" }, "step": { "user": { "data": { - "api_key": "API Key", - "name": "Name" + "api_key": "API Key" + } + }, + "reauth_confirm": { + "data": { + "api_key": "API Key" } } } diff --git a/tests/components/sensibo/test_config_flow.py b/tests/components/sensibo/test_config_flow.py index 9cc96c1c04b..cb5c2b239df 100644 --- a/tests/components/sensibo/test_config_flow.py +++ b/tests/components/sensibo/test_config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations import asyncio +from typing import Any from unittest.mock import patch import aiohttp @@ -233,3 +234,184 @@ async def test_flow_get_no_username(hass: HomeAssistant) -> None: ) assert result2["errors"] == {"base": "no_username"} + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + version=2, + domain=DOMAIN, + unique_id="username", + data={"api_key": "1234567890"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ) as mock_sensibo, patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == {"api_key": "1234567891"} + + assert len(mock_sensibo.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +@pytest.mark.parametrize( + "sideeffect,p_error", + [ + (aiohttp.ClientConnectionError, "cannot_connect"), + (asyncio.TimeoutError, "cannot_connect"), + (AuthenticationError, "invalid_auth"), + (SensiboError, "cannot_connect"), + ], +) +async def test_reauth_flow_error( + hass: HomeAssistant, sideeffect: Exception, p_error: str +) -> None: + """Test a reauthentication flow with error.""" + entry = MockConfigEntry( + version=2, + domain=DOMAIN, + unique_id="username", + data={"api_key": "1234567890"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + side_effect=sideeffect, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567890"}, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": p_error} + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value={"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value={"result": {"username": "username"}}, + ), patch( + "homeassistant.components.sensibo.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_API_KEY: "1234567891"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == {"api_key": "1234567891"} + + +@pytest.mark.parametrize( + "get_devices,get_me,p_error", + [ + ( + {"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + {"result": {}}, + "no_username", + ), + ( + {"result": []}, + {"result": {"username": "username"}}, + "no_devices", + ), + ( + {"result": [{"id": "xyzxyz"}, {"id": "abcabc"}]}, + {"result": {"username": "username2"}}, + "incorrect_api_key", + ), + ], +) +async def test_flow_reauth_no_username_or_device( + hass: HomeAssistant, + get_devices: dict[str, Any], + get_me: dict[str, Any], + p_error: str, +) -> None: + """Test config flow get no username from api.""" + entry = MockConfigEntry( + version=2, + domain=DOMAIN, + unique_id="username", + data={"api_key": "1234567890"}, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, + ) + + assert result["type"] == RESULT_TYPE_FORM + assert result["step_id"] == "reauth_confirm" + + with patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_devices", + return_value=get_devices, + ), patch( + "homeassistant.components.sensibo.util.SensiboClient.async_get_me", + return_value=get_me, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_API_KEY: "1234567890", + }, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": p_error}