Add reauth to Alexa Devices config flow (#147773)

This commit is contained in:
Simone Chemelli 2025-07-01 21:57:24 +03:00 committed by GitHub
parent 926e9261ab
commit 058f3b8b6e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
5 changed files with 160 additions and 12 deletions

View File

@ -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,
)

View File

@ -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

View File

@ -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

View File

@ -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": {

View File

@ -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"