1
0
mirror of https://github.com/home-assistant/core.git synced 2026-04-02 08:26:41 +01:00

feat: implement reauthentication requirement (#165641)

This commit is contained in:
Joshua Monta
2026-03-16 16:03:01 +08:00
committed by GitHub
parent 469e06fb8c
commit 91a43873a2
6 changed files with 128 additions and 10 deletions

View File

@@ -3,11 +3,11 @@
from aiodns.error import DNSError
from aiohttp.client_exceptions import ClientConnectionError
from uhooapi import Client
from uhooapi.errors import UhooError, UnauthorizedError
from uhooapi.errors import ForbiddenError, UhooError, UnauthorizedError
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import PLATFORMS
@@ -28,8 +28,8 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: UhooConfigEntry)
await client.setup_devices()
except (ClientConnectionError, DNSError) as err:
raise ConfigEntryNotReady(f"Cannot connect to uHoo servers: {err}") from err
except UnauthorizedError as err:
raise ConfigEntryError(f"Invalid API credentials: {err}") from err
except (UnauthorizedError, ForbiddenError) as err:
raise ConfigEntryAuthFailed(f"Invalid API credentials: {err}") from err
except UhooError as err:
raise ConfigEntryNotReady(err) from err

View File

@@ -1,9 +1,10 @@
"""Custom uhoo config flow setup."""
from collections.abc import Mapping
from typing import Any
from uhooapi import Client
from uhooapi.errors import UhooError, UnauthorizedError
from uhooapi.errors import ForbiddenError, UhooError, UnauthorizedError
import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
@@ -45,7 +46,7 @@ class UhooConfigFlow(ConfigFlow, domain=DOMAIN):
client = Client(user_input[CONF_API_KEY], session, debug=True)
try:
await client.login()
except UnauthorizedError:
except UnauthorizedError, ForbiddenError:
errors["base"] = "invalid_auth"
except UhooError:
errors["base"] = "cannot_connect"
@@ -65,3 +66,39 @@ class UhooConfigFlow(ConfigFlow, domain=DOMAIN):
),
errors=errors,
)
async def async_step_reauth(
self, entry_data: Mapping[str, Any]
) -> ConfigFlowResult:
"""Perform reauthentication upon an API authentication error."""
return await self.async_step_reauth_confirm()
async def async_step_reauth_confirm(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Confirm reauthentication dialog."""
errors: dict[str, str] = {}
if user_input is not None:
session = async_create_clientsession(self.hass)
client = Client(user_input[CONF_API_KEY], session, debug=True)
try:
await client.login()
except UnauthorizedError, ForbiddenError:
errors["base"] = "invalid_auth"
except UhooError:
errors["base"] = "cannot_connect"
except Exception: # noqa: BLE001
LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
return self.async_update_reload_and_abort(
self._get_reauth_entry(),
data_updates=user_input,
)
return self.async_show_form(
step_id="reauth_confirm",
data_schema=self.add_suggested_values_to_schema(
USER_DATA_SCHEMA, user_input
),
errors=errors,
)

View File

@@ -1,10 +1,11 @@
"""Custom uhoo data update coordinator."""
from uhooapi import Client, Device
from uhooapi.errors import UhooError
from uhooapi.errors import ForbiddenError, UhooError, UnauthorizedError
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 DOMAIN, LOGGER, UPDATE_INTERVAL
@@ -34,6 +35,8 @@ class UhooDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]):
if self.client.devices:
for device_id in self.client.devices:
await self.client.get_latest_data(device_id)
except (UnauthorizedError, ForbiddenError) as error:
raise ConfigEntryAuthFailed(f"Invalid API credentials: {error}") from error
except UhooError as error:
raise UpdateFailed(f"The device is unavailable: {error}") from error
else:

View File

@@ -28,7 +28,7 @@ rules:
integration-owner: done
log-when-unavailable: todo
parallel-updates: done
reauthentication-flow: todo
reauthentication-flow: done
test-coverage: done
# Gold

View File

@@ -1,7 +1,8 @@
{
"config": {
"abort": {
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]"
"already_configured": "[%key:common::config_flow::abort::already_configured_account%]",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
},
"error": {
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
@@ -9,6 +10,14 @@
"unknown": "[%key:common::config_flow::error::unknown%]"
},
"step": {
"reauth_confirm": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "Your uHoo API key. You can find this in your uHoo account settings."
}
},
"user": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"

View File

@@ -3,7 +3,7 @@
from unittest.mock import AsyncMock
import pytest
from uhooapi.errors import UhooError, UnauthorizedError
from uhooapi.errors import ForbiddenError, UhooError, UnauthorizedError
from homeassistant.components.uhoo.const import DOMAIN
from homeassistant.config_entries import SOURCE_USER
@@ -57,6 +57,7 @@ async def test_user_duplicate_entry(
[
(UhooError("asd"), "cannot_connect"),
(UnauthorizedError("Invalid credentials"), "invalid_auth"),
(ForbiddenError("Forbidden"), "invalid_auth"),
(Exception(), "unknown"),
],
)
@@ -94,3 +95,71 @@ async def test_user_flow_exceptions(
)
assert result["type"] is FlowResultType.CREATE_ENTRY
async def test_reauth_flow(
hass: HomeAssistant,
mock_uhoo_client: AsyncMock,
mock_config_entry: MockConfigEntry,
) -> None:
"""Test 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_API_KEY: "new-api-key-67890"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key-67890"
@pytest.mark.parametrize(
("exception", "error_type"),
[
(UnauthorizedError("Invalid credentials"), "invalid_auth"),
(ForbiddenError("Forbidden"), "invalid_auth"),
(UhooError("asd"), "cannot_connect"),
(Exception(), "unknown"),
],
)
async def test_reauth_flow_errors(
hass: HomeAssistant,
mock_uhoo_client: AsyncMock,
mock_config_entry: MockConfigEntry,
exception: Exception,
error_type: str,
) -> None:
"""Test reauthentication flow with errors and recovery."""
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_uhoo_client.login.side_effect = exception
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new-api-key-67890"},
)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
assert result["errors"] == {"base": error_type}
mock_uhoo_client.login.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new-api-key-67890"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new-api-key-67890"