From 058f3b8b6ee1b52ee24aaa7bc3d53401aac9105e Mon Sep 17 00:00:00 2001 From: Simone Chemelli Date: Tue, 1 Jul 2025 21:57:24 +0300 Subject: [PATCH] Add reauth to Alexa Devices config flow (#147773) --- .../components/alexa_devices/config_flow.py | 75 +++++++++++++++++-- .../components/alexa_devices/coordinator.py | 10 ++- .../alexa_devices/quality_scale.yaml | 2 +- .../components/alexa_devices/strings.json | 11 +++ .../alexa_devices/test_config_flow.py | 74 ++++++++++++++++++ 5 files changed, 160 insertions(+), 12 deletions(-) diff --git a/homeassistant/components/alexa_devices/config_flow.py b/homeassistant/components/alexa_devices/config_flow.py index 5add7ceb711..961f2760065 100644 --- a/homeassistant/components/alexa_devices/config_flow.py +++ b/homeassistant/components/alexa_devices/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping from typing import Any from aioamazondevices.api import AmazonEchoApi @@ -10,11 +11,36 @@ import voluptuous as vol from homeassistant.config_entries import ConfigFlow, ConfigFlowResult from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME +from homeassistant.core import HomeAssistant import homeassistant.helpers.config_validation as cv from homeassistant.helpers.selector import CountrySelector from .const import CONF_LOGIN_DATA, DOMAIN +STEP_REAUTH_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CODE): cv.string, + } +) + + +async def validate_input(hass: HomeAssistant, data: dict[str, Any]) -> dict[str, Any]: + """Validate the user input allows us to connect.""" + + api = AmazonEchoApi( + data[CONF_COUNTRY], + data[CONF_USERNAME], + data[CONF_PASSWORD], + ) + + try: + data = await api.login_mode_interactive(data[CONF_CODE]) + finally: + await api.close() + + return data + class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Alexa Devices.""" @@ -25,13 +51,8 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): """Handle the initial step.""" errors = {} if user_input: - client = AmazonEchoApi( - user_input[CONF_COUNTRY], - user_input[CONF_USERNAME], - user_input[CONF_PASSWORD], - ) try: - data = await client.login_mode_interactive(user_input[CONF_CODE]) + data = await validate_input(self.hass, user_input) except CannotConnect: errors["base"] = "cannot_connect" except CannotAuthenticate: @@ -44,8 +65,6 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): title=user_input[CONF_USERNAME], data=user_input | {CONF_LOGIN_DATA: data}, ) - finally: - await client.close() return self.async_show_form( step_id="user", @@ -61,3 +80,43 @@ class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN): } ), ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle reauth flow.""" + self.context["title_placeholders"] = {CONF_USERNAME: entry_data[CONF_USERNAME]} + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> ConfigFlowResult: + """Handle reauth confirm.""" + errors: dict[str, str] = {} + + reauth_entry = self._get_reauth_entry() + entry_data = reauth_entry.data + + if user_input is not None: + try: + await validate_input(self.hass, {**reauth_entry.data, **user_input}) + except CannotConnect: + errors["base"] = "cannot_connect" + except CannotAuthenticate: + errors["base"] = "invalid_auth" + else: + return self.async_update_reload_and_abort( + reauth_entry, + data={ + CONF_USERNAME: entry_data[CONF_USERNAME], + CONF_PASSWORD: entry_data[CONF_PASSWORD], + CONF_CODE: user_input[CONF_CODE], + }, + ) + + return self.async_show_form( + step_id="reauth_confirm", + description_placeholders={CONF_USERNAME: entry_data[CONF_USERNAME]}, + data_schema=STEP_REAUTH_DATA_SCHEMA, + errors=errors, + ) diff --git a/homeassistant/components/alexa_devices/coordinator.py b/homeassistant/components/alexa_devices/coordinator.py index 8e58441d46c..031f52abebf 100644 --- a/homeassistant/components/alexa_devices/coordinator.py +++ b/homeassistant/components/alexa_devices/coordinator.py @@ -12,10 +12,10 @@ from aioamazondevices.exceptions import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME 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 _LOGGER, CONF_LOGIN_DATA +from .const import _LOGGER, CONF_LOGIN_DATA, DOMAIN SCAN_INTERVAL = 30 @@ -55,4 +55,8 @@ class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]): except (CannotConnect, CannotRetrieveData) as err: raise UpdateFailed(f"Error occurred while updating {self.name}") from err except CannotAuthenticate as err: - raise ConfigEntryError("Could not authenticate") from err + raise ConfigEntryAuthFailed( + translation_domain=DOMAIN, + translation_key="invalid_auth", + translation_placeholders={"error": repr(err)}, + ) from err diff --git a/homeassistant/components/alexa_devices/quality_scale.yaml b/homeassistant/components/alexa_devices/quality_scale.yaml index afd12ca1df2..4662134efe8 100644 --- a/homeassistant/components/alexa_devices/quality_scale.yaml +++ b/homeassistant/components/alexa_devices/quality_scale.yaml @@ -34,7 +34,7 @@ rules: integration-owner: done log-when-unavailable: done parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: status: todo comment: all tests missing diff --git a/homeassistant/components/alexa_devices/strings.json b/homeassistant/components/alexa_devices/strings.json index d092cfaa2ae..89ab5b7056e 100644 --- a/homeassistant/components/alexa_devices/strings.json +++ b/homeassistant/components/alexa_devices/strings.json @@ -22,12 +22,23 @@ "password": "[%key:component::alexa_devices::common::data_description_password%]", "code": "[%key:component::alexa_devices::common::data_description_code%]" } + }, + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]", + "code": "[%key:component::alexa_devices::common::data_code%]" + }, + "data_description": { + "password": "[%key:component::alexa_devices::common::data_description_password%]", + "code": "[%key:component::alexa_devices::common::data_description_code%]" + } } }, "abort": { "already_configured": "[%key:common::config_flow::abort::already_configured_service%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" }, "error": { diff --git a/tests/components/alexa_devices/test_config_flow.py b/tests/components/alexa_devices/test_config_flow.py index 9bf174c5955..57049617986 100644 --- a/tests/components/alexa_devices/test_config_flow.py +++ b/tests/components/alexa_devices/test_config_flow.py @@ -133,3 +133,77 @@ async def test_already_configured( assert result["type"] is FlowResultType.ABORT assert result["reason"] == "already_configured" + + +async def test_reauth_successful( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test starting a reauthentication flow.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "other_fake_password", + CONF_CODE: "000000", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (CannotConnect, "cannot_connect"), + (CannotAuthenticate, "invalid_auth"), + ], +) +async def test_reauth_not_successful( + hass: HomeAssistant, + mock_amazon_devices_client: AsyncMock, + mock_setup_entry: AsyncMock, + mock_config_entry: MockConfigEntry, + side_effect: Exception, + error: str, +) -> None: + """Test starting a reauthentication flow but no connection found.""" + mock_config_entry.add_to_hass(hass) + result = await mock_config_entry.start_reauth_flow(hass) + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + mock_amazon_devices_client.login_mode_interactive.side_effect = side_effect + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "other_fake_password", + CONF_CODE: "000000", + }, + ) + + assert result["type"] is FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": error} + + mock_amazon_devices_client.login_mode_interactive.side_effect = None + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_PASSWORD: "fake_password", + CONF_CODE: "111111", + }, + ) + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_PASSWORD] == "fake_password" + assert mock_config_entry.data[CONF_CODE] == "111111"