diff --git a/homeassistant/components/ista_ecotrend/__init__.py b/homeassistant/components/ista_ecotrend/__init__.py index 5c1099f9d67..76ef8d13fd4 100644 --- a/homeassistant/components/ista_ecotrend/__init__.py +++ b/homeassistant/components/ista_ecotrend/__init__.py @@ -9,7 +9,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from .const import DOMAIN from .coordinator import IstaCoordinator @@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool translation_key="connection_exception", ) from e except (LoginError, KeycloakError) as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", translation_placeholders={CONF_EMAIL: entry.data[CONF_EMAIL]}, diff --git a/homeassistant/components/ista_ecotrend/config_flow.py b/homeassistant/components/ista_ecotrend/config_flow.py index 86696950484..b91f10eabdc 100644 --- a/homeassistant/components/ista_ecotrend/config_flow.py +++ b/homeassistant/components/ista_ecotrend/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import TYPE_CHECKING, Any @@ -9,13 +10,14 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_EMAIL, CONF_PASSWORD +from homeassistant.const import CONF_EMAIL, CONF_NAME, CONF_PASSWORD from homeassistant.helpers.selector import ( TextSelector, TextSelectorConfig, TextSelectorType, ) +from . import IstaConfigEntry from .const import DOMAIN _LOGGER = logging.getLogger(__name__) @@ -41,6 +43,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema( class IstaConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for ista EcoTrend.""" + reauth_entry: IstaConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> ConfigFlowResult: @@ -79,3 +83,57 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN): ), errors=errors, ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Dialog that informs the user that reauth is required.""" + errors: dict[str, str] = {} + if TYPE_CHECKING: + assert self.reauth_entry + + if user_input is not None: + ista = PyEcotrendIsta( + user_input[CONF_EMAIL], + user_input[CONF_PASSWORD], + _LOGGER, + ) + try: + await self.hass.async_add_executor_job(ista.login) + except (ServerError, InternalServerError): + errors["base"] = "cannot_connect" + except (LoginError, KeycloakError): + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + self.reauth_entry, data=user_input + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=self.add_suggested_values_to_schema( + data_schema=STEP_USER_DATA_SCHEMA, + suggested_values={ + CONF_EMAIL: user_input[CONF_EMAIL] + if user_input is not None + else self.reauth_entry.data[CONF_EMAIL] + }, + ), + description_placeholders={ + CONF_NAME: self.reauth_entry.title, + CONF_EMAIL: self.reauth_entry.data[CONF_EMAIL], + }, + errors=errors, + ) diff --git a/homeassistant/components/ista_ecotrend/coordinator.py b/homeassistant/components/ista_ecotrend/coordinator.py index b3be5883136..8d55574f0a1 100644 --- a/homeassistant/components/ista_ecotrend/coordinator.py +++ b/homeassistant/components/ista_ecotrend/coordinator.py @@ -10,7 +10,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr from homeassistant.const import CONF_EMAIL from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import DOMAIN @@ -45,7 +45,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): "Unable to connect and retrieve data from ista EcoTrend, try again later" ) from e except (LoginError, KeycloakError) as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 @@ -70,7 +70,7 @@ class IstaCoordinator(DataUpdateCoordinator[dict[str, Any]]): "Unable to connect and retrieve data from ista EcoTrend, try again later" ) from e except (LoginError, KeycloakError) as e: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( translation_domain=DOMAIN, translation_key="authentication_exception", translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 diff --git a/homeassistant/components/ista_ecotrend/strings.json b/homeassistant/components/ista_ecotrend/strings.json index af976e89e09..f76cf5286cb 100644 --- a/homeassistant/components/ista_ecotrend/strings.json +++ b/homeassistant/components/ista_ecotrend/strings.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", @@ -14,6 +15,14 @@ "email": "[%key:common::config_flow::data::email%]", "password": "[%key:common::config_flow::data::password%]" } + }, + "reauth_confirm": { + "title": "[%key:common::config_flow::title::reauth%]", + "description": "Please reenter the password for: {email}", + "data": { + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } } }, diff --git a/tests/components/ista_ecotrend/test_config_flow.py b/tests/components/ista_ecotrend/test_config_flow.py index 3375394f3f6..b702b0331e8 100644 --- a/tests/components/ista_ecotrend/test_config_flow.py +++ b/tests/components/ista_ecotrend/test_config_flow.py @@ -6,7 +6,7 @@ from pyecotrend_ista import LoginError, ServerError import pytest from homeassistant.components.ista_ecotrend.const import DOMAIN -from homeassistant.config_entries import SOURCE_USER +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_EMAIL, CONF_PASSWORD from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -87,3 +87,106 @@ async def test_form_invalid_auth( CONF_PASSWORD: "test-password", } assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth( + hass: HomeAssistant, + ista_config_entry: AsyncMock, + mock_ista: MagicMock, +) -> None: + """Test reauth flow.""" + + ista_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": ista_config_entry.entry_id, + "unique_id": ista_config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert ista_config_entry.data == { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + } + assert len(hass.config_entries.async_entries()) == 1 + + +@pytest.mark.parametrize( + ("side_effect", "error_text"), + [ + (LoginError(None), "invalid_auth"), + (ServerError, "cannot_connect"), + (IndexError, "unknown"), + ], +) +async def test_reauth_error_and_recover( + hass: HomeAssistant, + ista_config_entry: AsyncMock, + mock_ista: MagicMock, + side_effect: Exception, + error_text: str, +) -> None: + """Test reauth flow.""" + + ista_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "entry_id": ista_config_entry.entry_id, + "unique_id": ista_config_entry.unique_id, + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_ista.login.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_text} + + mock_ista.login.side_effect = None + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + }, + ) + + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert ista_config_entry.data == { + CONF_EMAIL: "new@example.com", + CONF_PASSWORD: "new-password", + } + assert len(hass.config_entries.async_entries()) == 1 diff --git a/tests/components/ista_ecotrend/test_init.py b/tests/components/ista_ecotrend/test_init.py index 642afc820dd..a15e4577252 100644 --- a/tests/components/ista_ecotrend/test_init.py +++ b/tests/components/ista_ecotrend/test_init.py @@ -6,7 +6,7 @@ from pyecotrend_ista import KeycloakError, LoginError, ParserError, ServerError import pytest from syrupy.assertion import SnapshotAssertion -from homeassistant.config_entries import ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -54,7 +54,7 @@ async def test_config_entry_not_ready( ("side_effect"), [LoginError, KeycloakError], ) -async def test_config_entry_error( +async def test_config_entry_auth_failed( hass: HomeAssistant, ista_config_entry: MockConfigEntry, mock_ista: MagicMock, @@ -67,6 +67,7 @@ async def test_config_entry_error( await hass.async_block_till_done() assert ista_config_entry.state is ConfigEntryState.SETUP_ERROR + assert any(ista_config_entry.async_get_active_flows(hass, {SOURCE_REAUTH})) @pytest.mark.usefixtures("mock_ista")