From 4cd7f9bd8b5315ad66246c7d048f8221fee4468f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 9 Apr 2021 19:41:29 -1000 Subject: [PATCH] Raise ConfigEntryAuthFailed during setup or coordinator update to start reauth (#48962) --- .coveragerc | 1 + homeassistant/components/abode/__init__.py | 15 +- .../components/airvisual/__init__.py | 25 +-- homeassistant/components/august/__init__.py | 34 +--- homeassistant/components/awair/__init__.py | 24 +-- homeassistant/components/awair/config_flow.py | 10 +- homeassistant/components/axis/device.py | 14 +- .../components/azure_devops/__init__.py | 14 +- .../components/azure_devops/config_flow.py | 21 +-- homeassistant/components/deconz/gateway.py | 14 +- .../components/fireservicerota/__init__.py | 20 +- .../components/fireservicerota/config_flow.py | 9 +- homeassistant/components/fritzbox/__init__.py | 14 +- .../components/fritzbox/config_flow.py | 10 +- homeassistant/components/hive/__init__.py | 16 +- homeassistant/components/hyperion/__init__.py | 23 +-- .../components/icloud/config_flow.py | 9 +- homeassistant/components/neato/__init__.py | 20 +- homeassistant/components/nest/__init__.py | 13 +- homeassistant/components/nuki/__init__.py | 11 +- .../components/ovo_energy/__init__.py | 18 +- .../components/ovo_energy/config_flow.py | 21 +-- homeassistant/components/plex/__init__.py | 28 +-- .../components/powerwall/__init__.py | 29 +-- .../components/sharkiq/config_flow.py | 9 +- .../components/sharkiq/update_coordinator.py | 28 +-- .../components/simplisafe/__init__.py | 27 +-- homeassistant/components/sonarr/__init__.py | 23 +-- .../components/sonarr/config_flow.py | 3 +- homeassistant/components/spotify/__init__.py | 12 +- homeassistant/components/tesla/__init__.py | 23 +-- .../components/totalconnect/__init__.py | 26 +-- homeassistant/components/unifi/config_flow.py | 5 +- homeassistant/components/unifi/controller.py | 14 +- homeassistant/components/verisure/__init__.py | 10 +- .../components/verisure/config_flow.py | 2 +- homeassistant/config_entries.py | 44 ++++- homeassistant/exceptions.py | 16 +- homeassistant/helpers/entity_platform.py | 2 - homeassistant/helpers/update_coordinator.py | 29 ++- tests/components/abode/test_init.py | 21 ++- tests/components/august/test_init.py | 26 +++ .../fireservicerota/test_config_flow.py | 39 ++++ tests/components/fritzbox/test_config_flow.py | 12 +- tests/components/fritzbox/test_init.py | 30 ++- tests/components/hyperion/test_light.py | 12 +- tests/components/sonarr/test_config_flow.py | 8 +- tests/components/sonarr/test_init.py | 8 +- tests/components/unifi/test_config_flow.py | 8 +- tests/components/verisure/test_config_flow.py | 24 ++- tests/test_config_entries.py | 177 +++++++++++++----- 51 files changed, 534 insertions(+), 517 deletions(-) diff --git a/.coveragerc b/.coveragerc index 6e3db6555ef..2a5e6ecc502 100644 --- a/.coveragerc +++ b/.coveragerc @@ -776,6 +776,7 @@ omit = homeassistant/components/poolsense/__init__.py homeassistant/components/poolsense/sensor.py homeassistant/components/poolsense/binary_sensor.py + homeassistant/components/powerwall/__init__.py homeassistant/components/proliphix/climate.py homeassistant/components/progettihwsw/__init__.py homeassistant/components/progettihwsw/binary_sensor.py diff --git a/homeassistant/components/abode/__init__.py b/homeassistant/components/abode/__init__.py index c1c89951c3f..329a0a679bc 100644 --- a/homeassistant/components/abode/__init__.py +++ b/homeassistant/components/abode/__init__.py @@ -9,7 +9,7 @@ import abodepy.helpers.timeline as TIMELINE from requests.exceptions import ConnectTimeout, HTTPError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_ATTRIBUTION, ATTR_DATE, @@ -20,7 +20,7 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_validation as cv from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.entity import Entity @@ -124,17 +124,10 @@ async def async_setup_entry(hass, config_entry): ) except AbodeAuthenticationException as ex: - LOGGER.error("Invalid credentials: %s", ex) - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=config_entry.data, - ) - return False + raise ConfigEntryAuthFailed(f"Invalid credentials: {ex}") from ex except (AbodeException, ConnectTimeout, HTTPError) as ex: - LOGGER.error("Unable to connect to Abode: %s", ex) - raise ConfigEntryNotReady from ex + raise ConfigEntryNotReady(f"Unable to connect to Abode: {ex}") from ex hass.data[DOMAIN] = AbodeSystem(abode, polling) diff --git a/homeassistant/components/airvisual/__init__.py b/homeassistant/components/airvisual/__init__.py index f02020d25b4..8447e62a15b 100644 --- a/homeassistant/components/airvisual/__init__.py +++ b/homeassistant/components/airvisual/__init__.py @@ -11,7 +11,6 @@ from pyairvisual.errors import ( NodeProError, ) -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_ATTRIBUTION, CONF_API_KEY, @@ -23,6 +22,7 @@ from homeassistant.const import ( CONF_STATE, ) from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -206,27 +206,8 @@ async def async_setup_entry(hass, config_entry): try: return await api_coro - except (InvalidKeyError, KeyExpiredError): - matching_flows = [ - flow - for flow in hass.config_entries.flow.async_progress() - if flow["context"]["source"] == SOURCE_REAUTH - and flow["context"]["unique_id"] == config_entry.unique_id - ] - - if not matching_flows: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": config_entry.unique_id, - }, - data=config_entry.data, - ) - ) - - return {} + except (InvalidKeyError, KeyExpiredError) as ex: + raise ConfigEntryAuthFailed from ex except AirVisualError as err: raise UpdateFailed(f"Error while retrieving data: {err}") from err diff --git a/homeassistant/components/august/__init__.py b/homeassistant/components/august/__init__.py index 46acd1132d9..041f24cc44f 100644 --- a/homeassistant/components/august/__init__.py +++ b/homeassistant/components/august/__init__.py @@ -8,10 +8,14 @@ from yalexs.exceptions import AugustApiAIOHTTPError from yalexs.pubnub_activity import activities_from_pubnub_message from yalexs.pubnub_async import AugustPubNub, async_create_pubnub -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_PASSWORD, HTTP_UNAUTHORIZED +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from .activity import ActivityStream from .const import DATA_AUGUST, DOMAIN, MIN_TIME_BETWEEN_DETAIL_UPDATES, PLATFORMS @@ -43,28 +47,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): try: await august_gateway.async_setup(entry.data) return await async_setup_august(hass, entry, august_gateway) - except ClientResponseError as err: - if err.status == HTTP_UNAUTHORIZED: - _async_start_reauth(hass, entry) - return False - + except (RequireValidation, InvalidAuth) as err: + raise ConfigEntryAuthFailed from err + except (ClientResponseError, CannotConnect, asyncio.TimeoutError) as err: raise ConfigEntryNotReady from err - except (RequireValidation, InvalidAuth): - _async_start_reauth(hass, entry) - return False - except (CannotConnect, asyncio.TimeoutError) as err: - raise ConfigEntryNotReady from err - - -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): diff --git a/homeassistant/components/awair/__init__.py b/homeassistant/components/awair/__init__.py index bfb95fd91fc..5b59e4d83ac 100644 --- a/homeassistant/components/awair/__init__.py +++ b/homeassistant/components/awair/__init__.py @@ -8,8 +8,8 @@ from async_timeout import timeout from python_awair import Awair from python_awair.exceptions import AuthError -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_ACCESS_TOKEN +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -74,27 +74,7 @@ class AwairDataUpdateCoordinator(DataUpdateCoordinator): ) return {result.device.uuid: result for result in results} except AuthError as err: - flow_context = { - "source": SOURCE_REAUTH, - "unique_id": self._config_entry.unique_id, - } - - matching_flows = [ - flow - for flow in self.hass.config_entries.flow.async_progress() - if flow["context"] == flow_context - ] - - if not matching_flows: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=self._config_entry.data, - ) - ) - - raise UpdateFailed(err) from err + raise ConfigEntryAuthFailed from err except Exception as err: raise UpdateFailed(err) from err diff --git a/homeassistant/components/awair/config_flow.py b/homeassistant/components/awair/config_flow.py index 76c7cbca3a9..466d45999f5 100644 --- a/homeassistant/components/awair/config_flow.py +++ b/homeassistant/components/awair/config_flow.py @@ -69,13 +69,9 @@ class AwairFlowHandler(ConfigFlow, domain=DOMAIN): _, error = await self._check_connection(access_token) if error is None: - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) - - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=user_input) + return self.async_abort(reason="reauth_successful") if error != "invalid_access_token": return self.async_abort(reason=error) diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index 93b63b64122..b2af9e0efc6 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -13,7 +13,6 @@ from axis.streammanager import SIGNAL_PLAYING, STATE_STOPPED from homeassistant.components import mqtt from homeassistant.components.mqtt import DOMAIN as MQTT_DOMAIN from homeassistant.components.mqtt.models import Message -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_NAME, @@ -23,7 +22,7 @@ from homeassistant.const import ( CONF_USERNAME, ) from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.httpx_client import get_async_client @@ -221,15 +220,8 @@ class AxisNetworkDevice: except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - AXIS_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry.data, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err self.fw_version = self.api.vapix.firmware_version self.product_type = self.api.vapix.product_type diff --git a/homeassistant/components/azure_devops/__init__.py b/homeassistant/components/azure_devops/__init__.py index 3db74679d9a..a971c06826c 100644 --- a/homeassistant/components/azure_devops/__init__.py +++ b/homeassistant/components/azure_devops/__init__.py @@ -14,8 +14,8 @@ from homeassistant.components.azure_devops.const import ( DATA_AZURE_DEVOPS_CLIENT, DOMAIN, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import ConfigType, HomeAssistantType @@ -30,17 +30,9 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool if entry.data[CONF_PAT] is not None: await client.authorize(entry.data[CONF_PAT], entry.data[CONF_ORG]) if not client.authorized: - _LOGGER.warning( + raise ConfigEntryAuthFailed( "Could not authorize with Azure DevOps. You may need to update your token" ) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False await client.get_project(entry.data[CONF_ORG], entry.data[CONF_PROJECT]) except aiohttp.ClientError as exception: _LOGGER.warning(exception) diff --git a/homeassistant/components/azure_devops/config_flow.py b/homeassistant/components/azure_devops/config_flow.py index 138ea67e788..8ca32193e63 100644 --- a/homeassistant/components/azure_devops/config_flow.py +++ b/homeassistant/components/azure_devops/config_flow.py @@ -105,17 +105,16 @@ class AzureDevOpsFlowHandler(ConfigFlow, domain=DOMAIN): if errors is not None: return await self._show_reauth_form(errors) - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_ORG: self._organization, - CONF_PROJECT: self._project, - CONF_PAT: self._pat, - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_ORG: self._organization, + CONF_PROJECT: self._project, + CONF_PAT: self._pat, + }, + ) + return self.async_abort(reason="reauth_successful") def _async_create_entry(self): """Handle create entry.""" diff --git a/homeassistant/components/deconz/gateway.py b/homeassistant/components/deconz/gateway.py index 2b38f6956be..93a0befa937 100644 --- a/homeassistant/components/deconz/gateway.py +++ b/homeassistant/components/deconz/gateway.py @@ -4,10 +4,9 @@ import asyncio import async_timeout from pydeconz import DeconzSession, errors -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC from homeassistant.helpers.dispatcher import async_dispatcher_send @@ -174,15 +173,8 @@ class DeconzGateway: except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DECONZ_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry.data, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err for platform in PLATFORMS: self.hass.async_create_task( diff --git a/homeassistant/components/fireservicerota/__init__.py b/homeassistant/components/fireservicerota/__init__.py index 593109b4f52..0a4936b6ed6 100644 --- a/homeassistant/components/fireservicerota/__init__.py +++ b/homeassistant/components/fireservicerota/__init__.py @@ -14,9 +14,10 @@ from pyfireservicerota import ( from homeassistant.components.binary_sensor import DOMAIN as BINARYSENSOR_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_TOKEN, CONF_URL, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.dispatcher import dispatcher_send from homeassistant.helpers.update_coordinator import DataUpdateCoordinator @@ -109,19 +110,10 @@ class FireServiceRotaOauth: self._fsr.refresh_tokens ) - except (InvalidAuthError, InvalidTokenError): - _LOGGER.error("Error refreshing tokens, triggered reauth workflow") - self._hass.async_create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={ - **self._entry.data, - }, - ) - ) - - return False + except (InvalidAuthError, InvalidTokenError) as err: + raise ConfigEntryAuthFailed( + "Error refreshing tokens, triggered reauth workflow" + ) from err _LOGGER.debug("Saving new tokens in config entry") self._hass.config_entries.async_update_entry( diff --git a/homeassistant/components/fireservicerota/config_flow.py b/homeassistant/components/fireservicerota/config_flow.py index be986744d6c..6d16c8513d8 100644 --- a/homeassistant/components/fireservicerota/config_flow.py +++ b/homeassistant/components/fireservicerota/config_flow.py @@ -82,11 +82,10 @@ class FireServiceRotaFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if step_id == "user": return self.async_create_entry(title=self._username, data=data) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") def _show_setup_form(self, user_input=None, errors=None, step_id="user"): """Show the setup form to the user.""" diff --git a/homeassistant/components/fritzbox/__init__.py b/homeassistant/components/fritzbox/__init__.py index 34f56ddc6f9..ff417b25daf 100644 --- a/homeassistant/components/fritzbox/__init__.py +++ b/homeassistant/components/fritzbox/__init__.py @@ -5,7 +5,7 @@ import socket from pyfritzhome import Fritzhome, LoginError import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -13,6 +13,7 @@ from homeassistant.const import ( CONF_USERNAME, EVENT_HOMEASSISTANT_STOP, ) +from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from .const import CONF_CONNECTIONS, DEFAULT_HOST, DEFAULT_USERNAME, DOMAIN, PLATFORMS @@ -80,15 +81,8 @@ async def async_setup_entry(hass, entry): try: await hass.async_add_executor_job(fritz.login) - except LoginError: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry, - ) - ) - return False + except LoginError as err: + raise ConfigEntryAuthFailed from err hass.data.setdefault(DOMAIN, {CONF_CONNECTIONS: {}, CONF_DEVICES: set()}) hass.data[DOMAIN][CONF_CONNECTIONS][entry.entry_id] = fritz diff --git a/homeassistant/components/fritzbox/config_flow.py b/homeassistant/components/fritzbox/config_flow.py index a462f885484..6a200ff22e4 100644 --- a/homeassistant/components/fritzbox/config_flow.py +++ b/homeassistant/components/fritzbox/config_flow.py @@ -170,12 +170,12 @@ class FritzboxConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors=errors, ) - async def async_step_reauth(self, entry): + async def async_step_reauth(self, data): """Trigger a reauthentication flow.""" - self._entry = entry - self._host = entry.data[CONF_HOST] - self._name = entry.data[CONF_HOST] - self._username = entry.data[CONF_USERNAME] + self._entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + self._host = data[CONF_HOST] + self._name = data[CONF_HOST] + self._username = data[CONF_USERNAME] return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/hive/__init__.py b/homeassistant/components/hive/__init__.py index 040ef7b4674..cc20b49b67a 100644 --- a/homeassistant/components/hive/__init__.py +++ b/homeassistant/components/hive/__init__.py @@ -10,7 +10,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.const import CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, @@ -77,18 +77,8 @@ async def async_setup_entry(hass, entry): except HTTPException as error: _LOGGER.error("Could not connect to the internet: %s", error) raise ConfigEntryNotReady() from error - except HiveReauthRequired: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": config_entries.SOURCE_REAUTH, - "unique_id": entry.unique_id, - }, - data=entry.data, - ) - ) - return False + except HiveReauthRequired as err: + raise ConfigEntryAuthFailed from err for ha_type, hive_type in PLATFORM_LOOKUP.items(): device_list = devices.get(hive_type) diff --git a/homeassistant/components/hyperion/__init__.py b/homeassistant/components/hyperion/__init__.py index 03b892ce83b..93f3c35f514 100644 --- a/homeassistant/components/hyperion/__init__.py +++ b/homeassistant/components/hyperion/__init__.py @@ -11,10 +11,10 @@ from hyperion import client, const as hyperion_const from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import CONF_HOST, CONF_PORT, CONF_SOURCE, CONF_TOKEN +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, @@ -109,17 +109,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return True -async def _create_reauth_flow( - hass: HomeAssistant, - config_entry: ConfigEntry, -) -> None: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_REAUTH}, data=config_entry.data - ) - ) - - @callback def listen_for_instance_updates( hass: HomeAssistant, @@ -181,14 +170,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b and token is None ): await hyperion_client.async_client_disconnect() - await _create_reauth_flow(hass, config_entry) - return False + raise ConfigEntryAuthFailed # Client login doesn't work? => Reauth. if not await hyperion_client.async_client_login(): await hyperion_client.async_client_disconnect() - await _create_reauth_flow(hass, config_entry) - return False + raise ConfigEntryAuthFailed # Cannot switch instance or cannot load state? => Not ready. if ( diff --git a/homeassistant/components/icloud/config_flow.py b/homeassistant/components/icloud/config_flow.py index 28570f3d93c..c26fb43e8b2 100644 --- a/homeassistant/components/icloud/config_flow.py +++ b/homeassistant/components/icloud/config_flow.py @@ -154,11 +154,10 @@ class IcloudFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): if step_id == "user": return self.async_create_entry(title=self._username, data=data) - for entry in self.hass.config_entries.async_entries(DOMAIN): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry(entry, data=data) - await self.hass.config_entries.async_reload(entry.entry_id) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=data) + await self.hass.config_entries.async_reload(entry.entry_id) + return self.async_abort(reason="reauth_successful") async def async_step_user(self, user_input=None): """Handle a flow initiated by the user.""" diff --git a/homeassistant/components/neato/__init__.py b/homeassistant/components/neato/__init__.py index bb0db8ebd85..9413ff77236 100644 --- a/homeassistant/components/neato/__init__.py +++ b/homeassistant/components/neato/__init__.py @@ -7,14 +7,9 @@ from pybotvac import Account, Neato from pybotvac.exceptions import NeatoException import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry -from homeassistant.const import ( - CONF_CLIENT_ID, - CONF_CLIENT_SECRET, - CONF_SOURCE, - CONF_TOKEN, -) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_CLIENT_ID, CONF_CLIENT_SECRET, CONF_TOKEN +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.util import Throttle @@ -74,14 +69,7 @@ async def async_setup(hass: HomeAssistantType, config: ConfigType) -> bool: async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool: """Set up config entry.""" if CONF_TOKEN not in entry.data: - # Init reauth flow - hass.async_create_task( - hass.config_entries.flow.async_init( - NEATO_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - ) - ) - return False + raise ConfigEntryAuthFailed implementation = ( await config_entry_oauth2_flow.async_get_config_entry_implementation( diff --git a/homeassistant/components/nest/__init__.py b/homeassistant/components/nest/__init__.py index cd3f6ed9ed3..42b167ee851 100644 --- a/homeassistant/components/nest/__init__.py +++ b/homeassistant/components/nest/__init__.py @@ -12,7 +12,7 @@ from google_nest_sdm.exceptions import ( from google_nest_sdm.google_nest_subscriber import GoogleNestSubscriber import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( CONF_BINARY_SENSORS, CONF_CLIENT_ID, @@ -22,7 +22,7 @@ from homeassistant.const import ( CONF_STRUCTURE, ) from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_entry_oauth2_flow, @@ -167,14 +167,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): await subscriber.start_async() except AuthException as err: _LOGGER.debug("Subscriber authentication error: %s", err) - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - return False + raise ConfigEntryAuthFailed from err except ConfigurationException as err: _LOGGER.error("Configuration error: %s", err) subscriber.stop_async() diff --git a/homeassistant/components/nuki/__init__.py b/homeassistant/components/nuki/__init__.py index a96cda07077..173beca0c4a 100644 --- a/homeassistant/components/nuki/__init__.py +++ b/homeassistant/components/nuki/__init__.py @@ -10,7 +10,7 @@ from pynuki.bridge import InvalidCredentialsException from requests.exceptions import RequestException from homeassistant import exceptions -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import CONF_HOST, CONF_PORT, CONF_TOKEN from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -85,13 +85,8 @@ async def async_setup_entry(hass, entry): ) locks, openers = await hass.async_add_executor_job(_get_bridge_devices, bridge) - except InvalidCredentialsException: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - return False + except InvalidCredentialsException as err: + raise exceptions.ConfigEntryAuthFailed from err except RequestException as err: raise exceptions.ConfigEntryNotReady from err diff --git a/homeassistant/components/ovo_energy/__init__.py b/homeassistant/components/ovo_energy/__init__.py index 98ed42ea10e..77fafef05ca 100644 --- a/homeassistant/components/ovo_energy/__init__.py +++ b/homeassistant/components/ovo_energy/__init__.py @@ -10,9 +10,9 @@ import async_timeout from ovoenergy import OVODailyUsage from ovoenergy.ovoenergy import OVOEnergy -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.typing import ConfigType, HomeAssistantType from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -44,12 +44,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool raise ConfigEntryNotReady from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - return False + raise ConfigEntryAuthFailed async def async_update_data() -> OVODailyUsage: """Fetch data from OVO Energy.""" @@ -61,12 +56,7 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool except aiohttp.ClientError as exception: raise UpdateFailed(exception) from exception if not authenticated: - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=entry.data - ) - ) - raise UpdateFailed("Not authenticated with OVO Energy") + raise ConfigEntryAuthFailed("Not authenticated with OVO Energy") return await client.get_daily_usage(datetime.utcnow().strftime("%Y-%m")) coordinator = DataUpdateCoordinator( diff --git a/homeassistant/components/ovo_energy/config_flow.py b/homeassistant/components/ovo_energy/config_flow.py index f65b8007ecb..25d66d93102 100644 --- a/homeassistant/components/ovo_energy/config_flow.py +++ b/homeassistant/components/ovo_energy/config_flow.py @@ -74,18 +74,15 @@ class OVOEnergyFlowHandler(ConfigFlow, domain=DOMAIN): errors["base"] = "connection_error" else: if authenticated: - await self.async_set_unique_id(self.username) - - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, - data={ - CONF_USERNAME: self.username, - CONF_PASSWORD: user_input[CONF_PASSWORD], - }, - ) - return self.async_abort(reason="reauth_successful") + entry = await self.async_set_unique_id(self.username) + self.hass.config_entries.async_update_entry( + entry, + data={ + CONF_USERNAME: self.username, + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + return self.async_abort(reason="reauth_successful") errors["base"] = "authorization_error" diff --git a/homeassistant/components/plex/__init__.py b/homeassistant/components/plex/__init__.py index 137c0524bac..ec2c6480776 100644 --- a/homeassistant/components/plex/__init__.py +++ b/homeassistant/components/plex/__init__.py @@ -15,15 +15,10 @@ from plexwebsocket import ( import requests.exceptions from homeassistant.components.media_player import DOMAIN as MP_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY, SOURCE_REAUTH -from homeassistant.const import ( - CONF_SOURCE, - CONF_URL, - CONF_VERIFY_SSL, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY +from homeassistant.const import CONF_URL, CONF_VERIFY_SSL, EVENT_HOMEASSISTANT_STOP from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dev_reg, entity_registry as ent_reg from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.debounce import Debouncer @@ -120,19 +115,10 @@ async def async_setup_entry(hass, entry): error, ) raise ConfigEntryNotReady from error - except plexapi.exceptions.Unauthorized: - hass.async_create_task( - hass.config_entries.flow.async_init( - PLEX_DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error( - "Token not accepted, please reauthenticate Plex server '%s'", - entry.data[CONF_SERVER], - ) - return False + except plexapi.exceptions.Unauthorized as ex: + raise ConfigEntryAuthFailed( + f"Token not accepted, please reauthenticate Plex server '{entry.data[CONF_SERVER]}'" + ) from ex except ( plexapi.exceptions.BadRequest, plexapi.exceptions.NotFound, diff --git a/homeassistant/components/powerwall/__init__.py b/homeassistant/components/powerwall/__init__.py index ceec56aa05a..6d61db659c8 100644 --- a/homeassistant/components/powerwall/__init__.py +++ b/homeassistant/components/powerwall/__init__.py @@ -11,10 +11,10 @@ from tesla_powerwall import ( PowerwallUnreachableError, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_IP_ADDRESS, CONF_PASSWORD from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity_registry import homeassistant.helpers.config_validation as cv from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed @@ -115,8 +115,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): except AccessDeniedError as err: _LOGGER.debug("Authentication failed", exc_info=err) http_session.close() - _async_start_reauth(hass, entry) - return False + raise ConfigEntryAuthFailed from err await _migrate_old_unique_ids(hass, entry_id, powerwall_data) @@ -130,13 +129,16 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): _LOGGER.debug("Updating data") try: return await _async_update_powerwall_data(hass, entry, power_wall) - except AccessDeniedError: + except AccessDeniedError as err: if password is None: - raise + raise ConfigEntryAuthFailed from err # If the session expired, relogin, and try again - await hass.async_add_executor_job(power_wall.login, "", password) - return await _async_update_powerwall_data(hass, entry, power_wall) + try: + await hass.async_add_executor_job(power_wall.login, "", password) + return await _async_update_powerwall_data(hass, entry, power_wall) + except AccessDeniedError as ex: + raise ConfigEntryAuthFailed from ex coordinator = DataUpdateCoordinator( hass, @@ -181,17 +183,6 @@ async def _async_update_powerwall_data( return hass.data[DOMAIN][entry.entry_id][POWERWALL_COORDINATOR].data -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Password is no longer valid. Please reauthenticate") - - def _login_and_fetch_base_info(power_wall: Powerwall, password: str): """Login to the powerwall and fetch the base info.""" if password is not None: diff --git a/homeassistant/components/sharkiq/config_flow.py b/homeassistant/components/sharkiq/config_flow.py index 046aaee7df5..962d29d7775 100644 --- a/homeassistant/components/sharkiq/config_flow.py +++ b/homeassistant/components/sharkiq/config_flow.py @@ -84,13 +84,10 @@ class SharkIqConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): _, errors = await self._async_validate_input(user_input) if not errors: - for entry in self._async_current_entries(): - if entry.unique_id == self.unique_id: - self.hass.config_entries.async_update_entry( - entry, data=user_input - ) + entry = await self.async_set_unique_id(self.unique_id) + self.hass.config_entries.async_update_entry(entry, data=user_input) - return self.async_abort(reason="reauth_successful") + return self.async_abort(reason="reauth_successful") if errors["base"] != "invalid_auth": return self.async_abort(reason=errors["base"]) diff --git a/homeassistant/components/sharkiq/update_coordinator.py b/homeassistant/components/sharkiq/update_coordinator.py index 73f4093739a..01490c39297 100644 --- a/homeassistant/components/sharkiq/update_coordinator.py +++ b/homeassistant/components/sharkiq/update_coordinator.py @@ -12,8 +12,9 @@ from sharkiqpy import ( SharkIqVacuum, ) -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import _LOGGER, API_TIMEOUT, DOMAIN, UPDATE_INTERVAL @@ -75,30 +76,7 @@ class SharkIqUpdateCoordinator(DataUpdateCoordinator): SharkIqAuthExpiringError, ) as err: _LOGGER.debug("Bad auth state. Attempting re-auth", exc_info=err) - flow_context = { - "source": SOURCE_REAUTH, - "unique_id": self._config_entry.unique_id, - } - - matching_flows = [ - flow - for flow in self.hass.config_entries.flow.async_progress() - if flow["context"] == flow_context - ] - - if not matching_flows: - _LOGGER.debug("Re-initializing flows. Attempting re-auth") - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - DOMAIN, - context=flow_context, - data=self._config_entry.data, - ) - ) - else: - _LOGGER.debug("Matching flow found") - - raise UpdateFailed(err) from err + raise ConfigEntryAuthFailed from err except Exception as err: _LOGGER.exception("Unexpected error updating SharkIQ") raise UpdateFailed(err) from err diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index 485284b3293..723c04caea0 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -17,7 +17,6 @@ from simplipy.websocket import ( ) import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( ATTR_CODE, CONF_CODE, @@ -26,7 +25,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import CoreState, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( aiohttp_client, config_validation as cv, @@ -514,27 +513,9 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): if self._emergency_refresh_token_used: - matching_flows = [ - flow - for flow in self._hass.config_entries.flow.async_progress() - if flow["context"].get("source") == SOURCE_REAUTH - and flow["context"].get("unique_id") - == self.config_entry.unique_id - ] - - if not matching_flows: - self._hass.async_create_task( - self._hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - "unique_id": self.config_entry.unique_id, - }, - data=self.config_entry.data, - ) - ) - - raise UpdateFailed("Update failed with stored refresh token") + raise ConfigEntryAuthFailed( + "Update failed with stored refresh token" + ) LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") self._emergency_refresh_token_used = True diff --git a/homeassistant/components/sonarr/__init__.py b/homeassistant/components/sonarr/__init__.py index 946d9b1e047..81053922034 100644 --- a/homeassistant/components/sonarr/__init__.py +++ b/homeassistant/components/sonarr/__init__.py @@ -8,17 +8,16 @@ from typing import Any from sonarr import Sonarr, SonarrAccessRestricted, SonarrError -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ( ATTR_NAME, CONF_API_KEY, CONF_HOST, CONF_PORT, - CONF_SOURCE, CONF_SSL, CONF_VERIFY_SSL, ) -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.entity import Entity from homeassistant.helpers.typing import HomeAssistantType @@ -73,9 +72,10 @@ async def async_setup_entry(hass: HomeAssistantType, entry: ConfigEntry) -> bool try: await sonarr.update() - except SonarrAccessRestricted: - _async_start_reauth(hass, entry) - return False + except SonarrAccessRestricted as err: + raise ConfigEntryAuthFailed( + "API Key is no longer valid. Please reauthenticate" + ) from err except SonarrError as err: raise ConfigEntryNotReady from err @@ -113,17 +113,6 @@ async def async_unload_entry(hass: HomeAssistantType, entry: ConfigEntry) -> boo return unload_ok -def _async_start_reauth(hass: HomeAssistantType, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data={"config_entry_id": entry.entry_id, **entry.data}, - ) - ) - _LOGGER.error("API Key is no longer valid. Please reauthenticate") - - async def _async_update_listener(hass: HomeAssistantType, entry: ConfigEntry) -> None: """Handle options update.""" await hass.config_entries.async_reload(entry.entry_id) diff --git a/homeassistant/components/sonarr/config_flow.py b/homeassistant/components/sonarr/config_flow.py index fd7315585dc..fe4cdd13454 100644 --- a/homeassistant/components/sonarr/config_flow.py +++ b/homeassistant/components/sonarr/config_flow.py @@ -79,7 +79,8 @@ class SonarrConfigFlow(ConfigFlow, domain=DOMAIN): """Handle configuration by re-auth.""" self._reauth = True self._entry_data = dict(data) - self._entry_id = self._entry_data.pop("config_entry_id") + entry = await self.async_set_unique_id(self.unique_id) + self._entry_id = entry.entry_id return await self.async_step_reauth_confirm() diff --git a/homeassistant/components/spotify/__init__.py b/homeassistant/components/spotify/__init__.py index e36491670f5..c4b8e30a8ba 100644 --- a/homeassistant/components/spotify/__init__.py +++ b/homeassistant/components/spotify/__init__.py @@ -6,10 +6,10 @@ import voluptuous as vol from homeassistant.components.media_player import DOMAIN as MEDIA_PLAYER_DOMAIN from homeassistant.components.spotify import config_flow -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import ATTR_CREDENTIALS, CONF_CLIENT_ID, CONF_CLIENT_SECRET from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import config_entry_oauth2_flow, config_validation as cv from homeassistant.helpers.config_entry_oauth2_flow import ( OAuth2Session, @@ -84,13 +84,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: } if not set(session.token["scope"].split(" ")).issuperset(SPOTIFY_SCOPES): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) + raise ConfigEntryAuthFailed hass.async_create_task( hass.config_entries.async_forward_entry_setup(entry, MEDIA_PLAYER_DOMAIN) diff --git a/homeassistant/components/tesla/__init__.py b/homeassistant/components/tesla/__init__.py index 5091d2ea102..11b96144ed6 100644 --- a/homeassistant/components/tesla/__init__.py +++ b/homeassistant/components/tesla/__init__.py @@ -9,7 +9,7 @@ from teslajsonpy import Controller as TeslaAPI from teslajsonpy.exceptions import IncompleteCredentials, TeslaException import voluptuous as vol -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT from homeassistant.const import ( ATTR_BATTERY_CHARGING, ATTR_BATTERY_LEVEL, @@ -20,7 +20,8 @@ from homeassistant.const import ( CONF_USERNAME, HTTP_UNAUTHORIZED, ) -from homeassistant.core import HomeAssistant, callback +from homeassistant.core import callback +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.helpers.update_coordinator import ( CoordinatorEntity, @@ -158,12 +159,11 @@ async def async_setup_entry(hass, config_entry): CONF_WAKE_ON_START, DEFAULT_WAKE_ON_START ) ) - except IncompleteCredentials: - _async_start_reauth(hass, config_entry) - return False + except IncompleteCredentials as ex: + raise ConfigEntryAuthFailed from ex except TeslaException as ex: if ex.code == HTTP_UNAUTHORIZED: - _async_start_reauth(hass, config_entry) + raise ConfigEntryAuthFailed from ex _LOGGER.error("Unable to communicate with Tesla API: %s", ex.message) return False _async_save_tokens(hass, config_entry, access_token, refresh_token) @@ -216,17 +216,6 @@ async def async_unload_entry(hass, config_entry) -> bool: return False -def _async_start_reauth(hass: HomeAssistant, entry: ConfigEntry): - hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data=entry.data, - ) - ) - _LOGGER.error("Credentials are no longer valid. Please reauthenticate") - - async def update_listener(hass, config_entry): """Update when config_entry options update.""" controller = hass.data[DOMAIN][config_entry.entry_id]["coordinator"].controller diff --git a/homeassistant/components/totalconnect/__init__.py b/homeassistant/components/totalconnect/__init__.py index 8ef223c49a5..4078655f075 100644 --- a/homeassistant/components/totalconnect/__init__.py +++ b/homeassistant/components/totalconnect/__init__.py @@ -5,9 +5,10 @@ import logging from total_connect_client import TotalConnectClient import voluptuous as vol -from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from .const import CONF_USERCODES, DOMAIN @@ -46,16 +47,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): password = conf[CONF_PASSWORD] if CONF_USERCODES not in conf: - _LOGGER.warning("No usercodes in TotalConnect configuration") # should only happen for those who used UI before we added usercodes - await hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - }, - data=conf, - ) - return False + raise ConfigEntryAuthFailed("No usercodes in TotalConnect configuration") temp_codes = conf[CONF_USERCODES] usercodes = {} @@ -67,18 +60,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): ) if not client.is_valid_credentials(): - _LOGGER.error("TotalConnect authentication failed") - await hass.async_create_task( - hass.config_entries.flow.async_init( - DOMAIN, - context={ - "source": SOURCE_REAUTH, - }, - data=conf, - ) - ) - - return False + raise ConfigEntryAuthFailed("TotalConnect authentication failed") hass.data[DOMAIN][entry.entry_id] = client diff --git a/homeassistant/components/unifi/config_flow.py b/homeassistant/components/unifi/config_flow.py index 094bae05881..2087f121928 100644 --- a/homeassistant/components/unifi/config_flow.py +++ b/homeassistant/components/unifi/config_flow.py @@ -192,8 +192,11 @@ class UnifiFlowHandler(config_entries.ConfigFlow, domain=UNIFI_DOMAIN): errors=errors, ) - async def async_step_reauth(self, config_entry: dict): + async def async_step_reauth(self, data: dict): """Trigger a reauthentication flow.""" + config_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) self.reauth_config_entry = config_entry self.context["title_placeholders"] = { diff --git a/homeassistant/components/unifi/controller.py b/homeassistant/components/unifi/controller.py index c77987bcbdd..e2ad9636d7a 100644 --- a/homeassistant/components/unifi/controller.py +++ b/homeassistant/components/unifi/controller.py @@ -30,7 +30,6 @@ from homeassistant.components.device_tracker import DOMAIN as TRACKER_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN from homeassistant.components.unifi.switch import BLOCK_SWITCH, POE_SWITCH -from homeassistant.config_entries import SOURCE_REAUTH from homeassistant.const import ( CONF_HOST, CONF_PASSWORD, @@ -39,7 +38,7 @@ from homeassistant.const import ( CONF_VERIFY_SSL, ) from homeassistant.core import callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.helpers.entity_registry import async_entries_for_config_entry @@ -323,15 +322,8 @@ class UniFiController: except CannotConnect as err: raise ConfigEntryNotReady from err - except AuthenticationRequired: - self.hass.async_create_task( - self.hass.config_entries.flow.async_init( - UNIFI_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=self.config_entry, - ) - ) - return False + except AuthenticationRequired as err: + raise ConfigEntryAuthFailed from err for site in sites.values(): if self.site == site["name"]: diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 32893aec88b..55e3d020b13 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -16,7 +16,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, @@ -24,6 +24,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed import homeassistant.helpers.config_validation as cv from homeassistant.helpers.storage import STORAGE_DIR @@ -124,12 +125,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): - await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": SOURCE_REAUTH}, - data={"entry": entry}, - ) - return False + raise ConfigEntryAuthFailed hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index 25560b62b16..3a434cd8b48 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -126,7 +126,7 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): async def async_step_reauth(self, data: dict[str, Any]) -> dict[str, Any]: """Handle initiation of re-authentication with Verisure.""" - self.entry = data["entry"] + 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( diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index 6ef14afb6a6..d689d4548a9 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -14,7 +14,11 @@ import attr from homeassistant import data_entry_flow, loader from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CALLBACK_TYPE, CoreState, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import device_registry, entity_registry from homeassistant.helpers.event import Event from homeassistant.helpers.typing import UNDEFINED, UndefinedType @@ -259,13 +263,26 @@ class ConfigEntry: "%s.async_setup_entry did not return boolean", integration.domain ) result = False + except ConfigEntryAuthFailed as ex: + message = str(ex) + auth_base_message = "could not authenticate" + auth_message = ( + f"{auth_base_message}: {message}" if message else auth_base_message + ) + _LOGGER.warning( + "Config entry '%s' for %s integration %s", + self.title, + self.domain, + auth_message, + ) + self._async_process_on_unload() + self.async_start_reauth(hass) + result = False except ConfigEntryNotReady as ex: self.state = ENTRY_STATE_SETUP_RETRY wait_time = 2 ** min(tries, 4) * 5 tries += 1 message = str(ex) - if not message and ex.__cause__: - message = str(ex.__cause__) ready_message = f"ready yet: {message}" if message else "ready yet" if tries == 1: _LOGGER.warning( @@ -494,6 +511,27 @@ class ConfigEntry: while self._on_unload: self._on_unload.pop()() + @callback + def async_start_reauth(self, hass: HomeAssistant) -> None: + """Start a reauth flow.""" + flow_context = { + "source": SOURCE_REAUTH, + "entry_id": self.entry_id, + "unique_id": self.unique_id, + } + + for flow in hass.config_entries.flow.async_progress(): + if flow["context"] == flow_context: + return + + hass.async_create_task( + hass.config_entries.flow.async_init( + self.domain, + context=flow_context, + data=self.data, + ) + ) + current_entry: ContextVar[ConfigEntry | None] = ContextVar( "current_entry", default=None diff --git a/homeassistant/exceptions.py b/homeassistant/exceptions.py index b40aa99520d..fba00e094cd 100644 --- a/homeassistant/exceptions.py +++ b/homeassistant/exceptions.py @@ -98,14 +98,26 @@ class ConditionErrorContainer(ConditionError): yield from item.output(indent) -class PlatformNotReady(HomeAssistantError): +class IntegrationError(HomeAssistantError): + """Base class for platform and config entry exceptions.""" + + def __str__(self) -> str: + """Return a human readable error.""" + return super().__str__() or str(self.__cause__) + + +class PlatformNotReady(IntegrationError): """Error to indicate that platform is not ready.""" -class ConfigEntryNotReady(HomeAssistantError): +class ConfigEntryNotReady(IntegrationError): """Error to indicate that config entry is not ready.""" +class ConfigEntryAuthFailed(IntegrationError): + """Error to indicate that config entry could not authenticate.""" + + class InvalidStateError(HomeAssistantError): """When an invalid state is encountered.""" diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index dc7386c18a8..490a5a2298c 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -228,8 +228,6 @@ class EntityPlatform: tries += 1 wait_time = min(tries, 6) * PLATFORM_NOT_READY_BASE_WAIT_TIME message = str(ex) - if not message and ex.__cause__: - message = str(ex.__cause__) ready_message = f"ready yet: {message}" if message else "ready yet" if tries == 1: logger.warning( diff --git a/homeassistant/helpers/update_coordinator.py b/homeassistant/helpers/update_coordinator.py index 53e92c433a9..37e234363b8 100644 --- a/homeassistant/helpers/update_coordinator.py +++ b/homeassistant/helpers/update_coordinator.py @@ -11,9 +11,10 @@ import urllib.error import aiohttp import requests +from homeassistant import config_entries from homeassistant.const import EVENT_HOMEASSISTANT_STOP from homeassistant.core import CALLBACK_TYPE, Event, HassJob, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import entity, event from homeassistant.util.dt import utcnow @@ -149,7 +150,7 @@ class DataUpdateCoordinator(Generic[T]): fails. Additionally logging is handled by config entry setup to ensure that multiple retries do not cause log spam. """ - await self._async_refresh(log_failures=False) + await self._async_refresh(log_failures=False, raise_on_auth_failed=True) if self.last_update_success: return ex = ConfigEntryNotReady() @@ -160,7 +161,9 @@ class DataUpdateCoordinator(Generic[T]): """Refresh data and log errors.""" await self._async_refresh(log_failures=True) - async def _async_refresh(self, log_failures: bool = True) -> None: + async def _async_refresh( + self, log_failures: bool = True, raise_on_auth_failed: bool = False + ) -> None: """Refresh data.""" if self._unsub_refresh: self._unsub_refresh() @@ -168,6 +171,7 @@ class DataUpdateCoordinator(Generic[T]): self._debounced_refresh.async_cancel() start = monotonic() + auth_failed = False try: self.data = await self._async_update_data() @@ -205,6 +209,23 @@ class DataUpdateCoordinator(Generic[T]): self.logger.error("Error fetching %s data: %s", self.name, err) self.last_update_success = False + except ConfigEntryAuthFailed as err: + auth_failed = True + self.last_exception = err + if self.last_update_success: + if log_failures: + self.logger.error( + "Authentication failed while fetching %s data: %s", + self.name, + err, + ) + self.last_update_success = False + if raise_on_auth_failed: + raise + + config_entry = config_entries.current_entry.get() + if config_entry: + config_entry.async_start_reauth(self.hass) except NotImplementedError as err: self.last_exception = err raise err @@ -228,7 +249,7 @@ class DataUpdateCoordinator(Generic[T]): self.name, monotonic() - start, ) - if self._listeners: + if not auth_failed and self._listeners: self._schedule_refresh() for update_callback in self._listeners: diff --git a/tests/components/abode/test_init.py b/tests/components/abode/test_init.py index b4f3dbd736b..41219f5ccef 100644 --- a/tests/components/abode/test_init.py +++ b/tests/components/abode/test_init.py @@ -1,8 +1,9 @@ """Tests for the Abode module.""" from unittest.mock import patch -from abodepy.exceptions import AbodeAuthenticationException +from abodepy.exceptions import AbodeAuthenticationException, AbodeException +from homeassistant import data_entry_flow from homeassistant.components.abode import ( DOMAIN as ABODE_DOMAIN, SERVICE_CAPTURE_IMAGE, @@ -10,6 +11,7 @@ from homeassistant.components.abode import ( SERVICE_TRIGGER_AUTOMATION, ) from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN +from homeassistant.config_entries import ENTRY_STATE_SETUP_RETRY from homeassistant.const import CONF_USERNAME, HTTP_BAD_REQUEST from .common import setup_platform @@ -68,8 +70,23 @@ async def test_invalid_credentials(hass): "homeassistant.components.abode.Abode", side_effect=AbodeAuthenticationException((HTTP_BAD_REQUEST, "auth error")), ), patch( - "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth" + "homeassistant.components.abode.config_flow.AbodeFlowHandler.async_step_reauth", + return_value={"type": data_entry_flow.RESULT_TYPE_FORM}, ) as mock_async_step_reauth: await setup_platform(hass, ALARM_DOMAIN) mock_async_step_reauth.assert_called_once() + + +async def test_raise_config_entry_not_ready_when_offline(hass): + """Config entry state is ENTRY_STATE_SETUP_RETRY when abode is offline.""" + with patch( + "homeassistant.components.abode.Abode", + side_effect=AbodeException("any"), + ): + config_entry = await setup_platform(hass, ALARM_DOMAIN) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_RETRY + + assert hass.config_entries.flow.async_progress() == [] diff --git a/tests/components/august/test_init.py b/tests/components/august/test_init.py index 8b0885f7341..bc9f0048738 100644 --- a/tests/components/august/test_init.py +++ b/tests/components/august/test_init.py @@ -271,6 +271,32 @@ async def test_requires_validation_state(hass): assert hass.config_entries.flow.async_progress()[0]["context"]["source"] == "reauth" +async def test_unknown_auth_http_401(hass): + """Config entry state is ENTRY_STATE_SETUP_ERROR when august gets an http.""" + + config_entry = MockConfigEntry( + domain=DOMAIN, + data=_mock_get_config()[DOMAIN], + title="August august", + ) + config_entry.add_to_hass(hass) + assert hass.config_entries.flow.async_progress() == [] + + await setup.async_setup_component(hass, "persistent_notification", {}) + with patch( + "yalexs.authenticator_async.AuthenticatorAsync.async_authenticate", + return_value=_mock_august_authentication("original_token", 1234, None), + ): + await hass.config_entries.async_setup(config_entry.entry_id) + await hass.async_block_till_done() + + assert config_entry.state == ENTRY_STATE_SETUP_ERROR + + flows = hass.config_entries.flow.async_progress() + + assert flows[0]["step_id"] == "reauth_validate" + + async def test_load_unload(hass): """Config entry can be unloaded.""" diff --git a/tests/components/fireservicerota/test_config_flow.py b/tests/components/fireservicerota/test_config_flow.py index 752adb2edc5..6f4fd21a534 100644 --- a/tests/components/fireservicerota/test_config_flow.py +++ b/tests/components/fireservicerota/test_config_flow.py @@ -107,3 +107,42 @@ async def test_step_user(hass): } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth(hass): + """Test the start of the config flow.""" + entry = MockConfigEntry( + domain=DOMAIN, data=MOCK_CONF, unique_id=MOCK_CONF[CONF_USERNAME] + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as mock_fsr: + mock_fireservicerota = mock_fsr.return_value + mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": "reauth", "unique_id": entry.unique_id}, + data=MOCK_CONF, + ) + + await hass.async_block_till_done() + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + + with patch( + "homeassistant.components.fireservicerota.config_flow.FireServiceRota" + ) as mock_fsr, patch( + "homeassistant.components.fireservicerota.async_setup_entry", + return_value=True, + ): + mock_fireservicerota = mock_fsr.return_value + mock_fireservicerota.request_tokens.return_value = MOCK_TOKEN_INFO + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "any"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" diff --git a/tests/components/fritzbox/test_config_flow.py b/tests/components/fritzbox/test_config_flow.py index f07a78e30de..5d3fcc181ce 100644 --- a/tests/components/fritzbox/test_config_flow.py +++ b/tests/components/fritzbox/test_config_flow.py @@ -103,7 +103,9 @@ async def test_reauth_success(hass: HomeAssistantType, fritz: Mock): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" @@ -130,7 +132,9 @@ async def test_reauth_auth_failed(hass: HomeAssistantType, fritz: Mock): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" @@ -156,7 +160,9 @@ async def test_reauth_not_successful(hass: HomeAssistantType, fritz: Mock): mock_config.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": SOURCE_REAUTH}, data=mock_config + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": mock_config.entry_id}, + data=mock_config.data, ) assert result["type"] == RESULT_TYPE_FORM assert result["step_id"] == "reauth_confirm" diff --git a/tests/components/fritzbox/test_init.py b/tests/components/fritzbox/test_init.py index 08655033f4d..dafb873fb8a 100644 --- a/tests/components/fritzbox/test_init.py +++ b/tests/components/fritzbox/test_init.py @@ -1,9 +1,15 @@ """Tests for the AVM Fritz!Box integration.""" -from unittest.mock import Mock, call +from unittest.mock import Mock, call, patch + +from pyfritzhome import LoginError from homeassistant.components.fritzbox.const import DOMAIN as FB_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import ENTRY_STATE_LOADED, ENTRY_STATE_NOT_LOADED +from homeassistant.config_entries import ( + ENTRY_STATE_LOADED, + ENTRY_STATE_NOT_LOADED, + ENTRY_STATE_SETUP_ERROR, +) from homeassistant.const import ( CONF_DEVICES, CONF_HOST, @@ -88,3 +94,23 @@ async def test_unload_remove(hass: HomeAssistantType, fritz: Mock): assert entry.state == ENTRY_STATE_NOT_LOADED state = hass.states.get(entity_id) assert state is None + + +async def test_raise_config_entry_not_ready_when_offline(hass): + """Config entry state is ENTRY_STATE_SETUP_RETRY when fritzbox is offline.""" + entry = MockConfigEntry( + domain=FB_DOMAIN, + data={CONF_HOST: "any", **MOCK_CONFIG[FB_DOMAIN][CONF_DEVICES][0]}, + unique_id="any", + ) + entry.add_to_hass(hass) + with patch( + "homeassistant.components.fritzbox.Fritzhome.login", + side_effect=LoginError("user"), + ): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + entries = hass.config_entries.async_entries() + config_entry = entries[0] + assert config_entry.state == ENTRY_STATE_SETUP_ERROR diff --git a/tests/components/hyperion/test_light.py b/tests/components/hyperion/test_light.py index bb8fe8d0814..505896fbe07 100644 --- a/tests/components/hyperion/test_light.py +++ b/tests/components/hyperion/test_light.py @@ -761,7 +761,11 @@ async def test_setup_entry_no_token_reauth(hass: HomeAssistantType) -> None: assert client.async_client_disconnect.called mock_flow_init.assert_called_once_with( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, data=config_entry.data, ) assert config_entry.state == ENTRY_STATE_SETUP_ERROR @@ -785,7 +789,11 @@ async def test_setup_entry_bad_token_reauth(hass: HomeAssistantType) -> None: assert client.async_client_disconnect.called mock_flow_init.assert_called_once_with( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": config_entry.entry_id, + "unique_id": config_entry.unique_id, + }, data=config_entry.data, ) assert config_entry.state == ENTRY_STATE_SETUP_ERROR diff --git a/tests/components/sonarr/test_config_flow.py b/tests/components/sonarr/test_config_flow.py index 701580ab37c..5f32e72aee1 100644 --- a/tests/components/sonarr/test_config_flow.py +++ b/tests/components/sonarr/test_config_flow.py @@ -101,13 +101,15 @@ async def test_full_reauth_flow_implementation( hass: HomeAssistantType, aioclient_mock: AiohttpClientMocker ) -> None: """Test the manual reauth flow from start to finish.""" - entry = await setup_integration(hass, aioclient_mock, skip_entry_setup=True) + entry = await setup_integration( + hass, aioclient_mock, skip_entry_setup=True, unique_id="any" + ) assert entry result = await hass.config_entries.flow.async_init( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data={"config_entry_id": entry.entry_id, **entry.data}, + context={CONF_SOURCE: SOURCE_REAUTH, "unique_id": entry.unique_id}, + data=entry.data, ) assert result["type"] == RESULT_TYPE_FORM diff --git a/tests/components/sonarr/test_init.py b/tests/components/sonarr/test_init.py index 16d33a23072..0e9c253f1b8 100644 --- a/tests/components/sonarr/test_init.py +++ b/tests/components/sonarr/test_init.py @@ -35,8 +35,12 @@ async def test_config_entry_reauth( mock_flow_init.assert_called_once_with( DOMAIN, - context={CONF_SOURCE: SOURCE_REAUTH}, - data={"config_entry_id": entry.entry_id, **entry.data}, + context={ + CONF_SOURCE: SOURCE_REAUTH, + "entry_id": entry.entry_id, + "unique_id": entry.unique_id, + }, + data=entry.data, ) diff --git a/tests/components/unifi/test_config_flow.py b/tests/components/unifi/test_config_flow.py index 106c1852414..43d14981bbb 100644 --- a/tests/components/unifi/test_config_flow.py +++ b/tests/components/unifi/test_config_flow.py @@ -380,8 +380,12 @@ async def test_reauth_flow_update_configuration(hass, aioclient_mock): result = await hass.config_entries.flow.async_init( UNIFI_DOMAIN, - context={"source": SOURCE_REAUTH}, - data=config_entry, + context={ + "source": SOURCE_REAUTH, + "unique_id": config_entry.unique_id, + "entry_id": config_entry.entry_id, + }, + data=config_entry.data, ) assert result["type"] == data_entry_flow.RESULT_TYPE_FORM diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index c53c418c72b..b9af9450132 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -204,7 +204,13 @@ async def test_reauth_flow(hass: HomeAssistant) -> None: entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + 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 @@ -255,7 +261,13 @@ async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, ) with patch( @@ -290,7 +302,13 @@ async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": entry.unique_id, + "entry_id": entry.entry_id, + }, + data=entry.data, ) with patch( diff --git a/tests/test_config_entries.py b/tests/test_config_entries.py index 24d635d52a3..dbfe48129c1 100644 --- a/tests/test_config_entries.py +++ b/tests/test_config_entries.py @@ -1,16 +1,21 @@ """Test the config manager.""" import asyncio from datetime import timedelta +import logging from unittest.mock import AsyncMock, Mock, patch import pytest from homeassistant import config_entries, data_entry_flow, loader -from homeassistant.const import EVENT_HOMEASSISTANT_CLOSE, EVENT_HOMEASSISTANT_STARTED +from homeassistant.const import EVENT_HOMEASSISTANT_STARTED from homeassistant.core import CoreState, callback -from homeassistant.exceptions import ConfigEntryNotReady, HomeAssistantError +from homeassistant.exceptions import ( + ConfigEntryAuthFailed, + ConfigEntryNotReady, + HomeAssistantError, +) from homeassistant.helpers import entity_registry as er -from homeassistant.helpers.aiohttp_client import async_create_clientsession +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from homeassistant.setup import async_setup_component from homeassistant.util import dt @@ -36,6 +41,10 @@ def mock_handlers(): VERSION = 1 + async def async_step_reauth(self, data): + """Mock Reauth.""" + return self.async_show_form(step_id="reauth") + with patch.dict( config_entries.HANDLERS, {"comp": MockFlowHandler, "test": MockFlowHandler} ): @@ -2531,56 +2540,130 @@ async def test_entry_reload_calls_on_unload_listeners(hass, manager): assert entry.state == config_entries.ENTRY_STATE_LOADED -async def test_entry_reload_cleans_up_aiohttp_session(hass, manager): - """Test reload cleans up aiohttp sessions their close listener created by the config entry.""" - entry = MockConfigEntry(domain="comp", state=config_entries.ENTRY_STATE_LOADED) - entry.add_to_hass(hass) - async_setup_calls = 0 +async def test_setup_raise_auth_failed(hass, caplog): + """Test a setup raising ConfigEntryAuthFailed.""" + entry = MockConfigEntry(title="test_title", domain="test") - async def async_setup_entry(hass, _): - """Mock setup entry.""" - nonlocal async_setup_calls - async_setup_calls += 1 - async_create_clientsession(hass) + mock_setup_entry = AsyncMock( + side_effect=ConfigEntryAuthFailed("The password is no longer valid") + ) + mock_integration(hass, MockModule("test", async_setup_entry=mock_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text + + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + + caplog.clear() + entry.state = config_entries.ENTRY_STATE_NOT_LOADED + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text + + # Verify multiple ConfigEntryAuthFailed does not generate a second flow + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + +async def test_setup_raise_auth_failed_from_first_coordinator_update(hass, caplog): + """Test async_config_entry_first_refresh raises ConfigEntryAuthFailed.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryAuthFailed("The password is no longer valid") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_config_entry_first_refresh() return True - async_setup = AsyncMock(return_value=True) - async_unload_entry = AsyncMock(return_value=True) + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) - mock_integration( - hass, - MockModule( - "comp", - async_setup=async_setup, - async_setup_entry=async_setup_entry, - async_unload_entry=async_unload_entry, - ), - ) - mock_entity_platform(hass, "config_flow.comp", None) + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text + + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH + + caplog.clear() + entry.state = config_entries.ENTRY_STATE_NOT_LOADED + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "could not authenticate: The password is no longer valid" in caplog.text + + # Verify multiple ConfigEntryAuthFailed does not generate a second flow + assert entry.state == config_entries.ENTRY_STATE_SETUP_ERROR + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + + +async def test_setup_raise_auth_failed_from_future_coordinator_update(hass, caplog): + """Test a coordinator raises ConfigEntryAuthFailed in the future.""" + entry = MockConfigEntry(title="test_title", domain="test") + + async def async_setup_entry(hass, entry): + """Mock setup entry with a simple coordinator.""" + + async def _async_update_data(): + raise ConfigEntryAuthFailed("The password is no longer valid") + + coordinator = DataUpdateCoordinator( + hass, + logging.getLogger(__name__), + name="any", + update_method=_async_update_data, + update_interval=timedelta(seconds=1000), + ) + + await coordinator.async_refresh() + return True + + mock_integration(hass, MockModule("test", async_setup_entry=async_setup_entry)) + mock_entity_platform(hass, "config_flow.test", None) + + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "Authentication failed while fetching" in caplog.text + assert "The password is no longer valid" in caplog.text - assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 1 - assert async_setup_calls == 1 assert entry.state == config_entries.ENTRY_STATE_LOADED + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1 + assert flows[0]["context"]["entry_id"] == entry.entry_id + assert flows[0]["context"]["source"] == config_entries.SOURCE_REAUTH - original_close_listeners = hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] + caplog.clear() + entry.state = config_entries.ENTRY_STATE_NOT_LOADED - assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 2 - assert async_setup_calls == 2 + await entry.async_setup(hass) + await hass.async_block_till_done() + assert "Authentication failed while fetching" in caplog.text + assert "The password is no longer valid" in caplog.text + + # Verify multiple ConfigEntryAuthFailed does not generate a second flow assert entry.state == config_entries.ENTRY_STATE_LOADED - - assert ( - hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] - == original_close_listeners - ) - - assert await manager.async_reload(entry.entry_id) - assert len(async_unload_entry.mock_calls) == 3 - assert async_setup_calls == 3 - assert entry.state == config_entries.ENTRY_STATE_LOADED - - assert ( - hass.bus.async_listeners()[EVENT_HOMEASSISTANT_CLOSE] - == original_close_listeners - ) + flows = hass.config_entries.flow.async_progress() + assert len(flows) == 1