Add reauth flow to ista EcoTrend integration (#118955)

This commit is contained in:
Mr. Bubbles 2024-06-21 13:25:24 +02:00 committed by GitHub
parent 18767154df
commit f5f2e04126
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 181 additions and 10 deletions

View File

@ -9,7 +9,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform from homeassistant.const import CONF_EMAIL, CONF_PASSWORD, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from .const import DOMAIN from .const import DOMAIN
from .coordinator import IstaCoordinator from .coordinator import IstaCoordinator
@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: IstaConfigEntry) -> bool
translation_key="connection_exception", translation_key="connection_exception",
) from e ) from e
except (LoginError, KeycloakError) as e: except (LoginError, KeycloakError) as e:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="authentication_exception", translation_key="authentication_exception",
translation_placeholders={CONF_EMAIL: entry.data[CONF_EMAIL]}, translation_placeholders={CONF_EMAIL: entry.data[CONF_EMAIL]},

View File

@ -2,6 +2,7 @@
from __future__ import annotations from __future__ import annotations
from collections.abc import Mapping
import logging import logging
from typing import TYPE_CHECKING, Any from typing import TYPE_CHECKING, Any
@ -9,13 +10,14 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr
import voluptuous as vol import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult 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 ( from homeassistant.helpers.selector import (
TextSelector, TextSelector,
TextSelectorConfig, TextSelectorConfig,
TextSelectorType, TextSelectorType,
) )
from . import IstaConfigEntry
from .const import DOMAIN from .const import DOMAIN
_LOGGER = logging.getLogger(__name__) _LOGGER = logging.getLogger(__name__)
@ -41,6 +43,8 @@ STEP_USER_DATA_SCHEMA = vol.Schema(
class IstaConfigFlow(ConfigFlow, domain=DOMAIN): class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
"""Handle a config flow for ista EcoTrend.""" """Handle a config flow for ista EcoTrend."""
reauth_entry: IstaConfigEntry | None = None
async def async_step_user( async def async_step_user(
self, user_input: dict[str, Any] | None = None self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult: ) -> ConfigFlowResult:
@ -79,3 +83,57 @@ class IstaConfigFlow(ConfigFlow, domain=DOMAIN):
), ),
errors=errors, 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,
)

View File

@ -10,7 +10,7 @@ from pyecotrend_ista import KeycloakError, LoginError, PyEcotrendIsta, ServerErr
from homeassistant.const import CONF_EMAIL from homeassistant.const import CONF_EMAIL
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError from homeassistant.exceptions import ConfigEntryAuthFailed
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from .const import DOMAIN 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" "Unable to connect and retrieve data from ista EcoTrend, try again later"
) from e ) from e
except (LoginError, KeycloakError) as e: except (LoginError, KeycloakError) as e:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="authentication_exception", translation_key="authentication_exception",
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 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" "Unable to connect and retrieve data from ista EcoTrend, try again later"
) from e ) from e
except (LoginError, KeycloakError) as e: except (LoginError, KeycloakError) as e:
raise ConfigEntryError( raise ConfigEntryAuthFailed(
translation_domain=DOMAIN, translation_domain=DOMAIN,
translation_key="authentication_exception", translation_key="authentication_exception",
translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001 translation_placeholders={CONF_EMAIL: self.ista._email}, # noqa: SLF001

View File

@ -1,7 +1,8 @@
{ {
"config": { "config": {
"abort": { "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": { "error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@ -14,6 +15,14 @@
"email": "[%key:common::config_flow::data::email%]", "email": "[%key:common::config_flow::data::email%]",
"password": "[%key:common::config_flow::data::password%]" "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%]"
}
} }
} }
}, },

View File

@ -6,7 +6,7 @@ from pyecotrend_ista import LoginError, ServerError
import pytest import pytest
from homeassistant.components.ista_ecotrend.const import DOMAIN 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.const import CONF_EMAIL, CONF_PASSWORD
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
@ -87,3 +87,106 @@ async def test_form_invalid_auth(
CONF_PASSWORD: "test-password", CONF_PASSWORD: "test-password",
} }
assert len(mock_setup_entry.mock_calls) == 1 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

View File

@ -6,7 +6,7 @@ from pyecotrend_ista import KeycloakError, LoginError, ParserError, ServerError
import pytest import pytest
from syrupy.assertion import SnapshotAssertion 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.core import HomeAssistant
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
@ -54,7 +54,7 @@ async def test_config_entry_not_ready(
("side_effect"), ("side_effect"),
[LoginError, KeycloakError], [LoginError, KeycloakError],
) )
async def test_config_entry_error( async def test_config_entry_auth_failed(
hass: HomeAssistant, hass: HomeAssistant,
ista_config_entry: MockConfigEntry, ista_config_entry: MockConfigEntry,
mock_ista: MagicMock, mock_ista: MagicMock,
@ -67,6 +67,7 @@ async def test_config_entry_error(
await hass.async_block_till_done() await hass.async_block_till_done()
assert ista_config_entry.state is ConfigEntryState.SETUP_ERROR 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") @pytest.mark.usefixtures("mock_ista")