From 66b1728c13e905fa7b6d952db286065e775e60c3 Mon Sep 17 00:00:00 2001 From: mettolen <1007649+mettolen@users.noreply.github.com> Date: Fri, 20 Mar 2026 11:03:23 +0200 Subject: [PATCH] Implement reauth for Huum integration (#165971) --- homeassistant/components/huum/config_flow.py | 46 ++++++++++++ homeassistant/components/huum/coordinator.py | 5 +- .../components/huum/quality_scale.yaml | 2 +- homeassistant/components/huum/strings.json | 13 +++- tests/components/huum/test_config_flow.py | 73 +++++++++++++++++++ tests/components/huum/test_init.py | 28 ++++++- 6 files changed, 162 insertions(+), 5 deletions(-) diff --git a/homeassistant/components/huum/config_flow.py b/homeassistant/components/huum/config_flow.py index 3564b0bb3bf..c5cdc18107a 100644 --- a/homeassistant/components/huum/config_flow.py +++ b/homeassistant/components/huum/config_flow.py @@ -2,6 +2,7 @@ from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -57,3 +58,48 @@ class HuumConfigFlow(ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors ) + + async def async_step_reauth( + self, entry_data: Mapping[str, Any] + ) -> ConfigFlowResult: + """Handle 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] = {} + reauth_entry = self._get_reauth_entry() + + if user_input is not None: + huum = Huum( + reauth_entry.data[CONF_USERNAME], + user_input[CONF_PASSWORD], + session=async_get_clientsession(self.hass), + ) + try: + await huum.status() + except Forbidden, NotAuthenticated: + errors["base"] = "invalid_auth" + except Exception: + _LOGGER.exception("Unknown error") + errors["base"] = "unknown" + else: + return self.async_update_reload_and_abort( + reauth_entry, + data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]}, + ) + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required(CONF_PASSWORD): str, + } + ), + description_placeholders={ + "username": reauth_entry.data[CONF_USERNAME], + }, + errors=errors, + ) diff --git a/homeassistant/components/huum/coordinator.py b/homeassistant/components/huum/coordinator.py index 96a670f7657..532e78a8175 100644 --- a/homeassistant/components/huum/coordinator.py +++ b/homeassistant/components/huum/coordinator.py @@ -12,8 +12,9 @@ from huum.schemas import HuumStatusResponse from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.aiohttp_client import async_get_clientsession -from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator from .const import DOMAIN @@ -54,6 +55,6 @@ class HuumDataUpdateCoordinator(DataUpdateCoordinator[HuumStatusResponse]): try: return await self.huum.status() except (Forbidden, NotAuthenticated) as err: - raise UpdateFailed( + raise ConfigEntryAuthFailed( "Could not log in to Huum with given credentials" ) from err diff --git a/homeassistant/components/huum/quality_scale.yaml b/homeassistant/components/huum/quality_scale.yaml index 872fd0365f1..d2d75fd86c5 100644 --- a/homeassistant/components/huum/quality_scale.yaml +++ b/homeassistant/components/huum/quality_scale.yaml @@ -38,7 +38,7 @@ rules: integration-owner: done log-when-unavailable: done parallel-updates: done - reauthentication-flow: todo + reauthentication-flow: done test-coverage: status: todo comment: | diff --git a/homeassistant/components/huum/strings.json b/homeassistant/components/huum/strings.json index 3d565e693a9..9ad89b5daaf 100644 --- a/homeassistant/components/huum/strings.json +++ b/homeassistant/components/huum/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%]", @@ -9,6 +10,16 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "step": { + "reauth_confirm": { + "data": { + "password": "[%key:common::config_flow::data::password%]" + }, + "data_description": { + "password": "[%key:component::huum::config::step::user::data_description::password%]" + }, + "description": "The authentication for {username} is no longer valid. Please enter the current password.", + "title": "[%key:common::config_flow::title::reauth%]" + }, "user": { "data": { "password": "[%key:common::config_flow::data::password%]", diff --git a/tests/components/huum/test_config_flow.py b/tests/components/huum/test_config_flow.py index 66f630f17b0..627ac3cce1a 100644 --- a/tests/components/huum/test_config_flow.py +++ b/tests/components/huum/test_config_flow.py @@ -115,3 +115,76 @@ async def test_huum_errors( }, ) assert result["type"] is FlowResultType.CREATE_ENTRY + + +async def test_reauth_flow( + hass: HomeAssistant, + mock_huum: AsyncMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test reauthentication flow succeeds with valid credentials.""" + 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" + assert result["errors"] == {} + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new_password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_USERNAME] == TEST_USERNAME + assert mock_config_entry.data[CONF_PASSWORD] == "new_password" + + +@pytest.mark.parametrize( + ( + "raises", + "error_base", + ), + [ + (Exception, "unknown"), + (Forbidden, "invalid_auth"), + ], +) +async def test_reauth_errors( + hass: HomeAssistant, + mock_huum: AsyncMock, + mock_config_entry: MockConfigEntry, + raises: Exception, + error_base: str, +) -> None: + """Test reauthentication flow handles errors and recovers.""" + mock_config_entry.add_to_hass(hass) + + result = await mock_config_entry.start_reauth_flow(hass) + + with patch( + "homeassistant.components.huum.config_flow.Huum.status", + side_effect=raises, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "wrong_password"}, + ) + + assert result["type"] is FlowResultType.FORM + assert result["errors"] == {"base": error_base} + + # Recover with valid credentials + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new_password"}, + ) + await hass.async_block_till_done() + + assert result["type"] is FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_USERNAME] == TEST_USERNAME + assert mock_config_entry.data[CONF_PASSWORD] == "new_password" diff --git a/tests/components/huum/test_init.py b/tests/components/huum/test_init.py index fac5fa875ee..5dc21232554 100644 --- a/tests/components/huum/test_init.py +++ b/tests/components/huum/test_init.py @@ -1,7 +1,11 @@ """Tests for the Huum __init__.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch +from huum.exceptions import Forbidden, NotAuthenticated +import pytest + +from homeassistant import config_entries from homeassistant.components.huum.const import DOMAIN from homeassistant.config_entries import ConfigEntryState from homeassistant.const import Platform @@ -25,3 +29,25 @@ async def test_loading_and_unloading_config_entry( await hass.async_block_till_done() assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + + +@pytest.mark.parametrize("side_effect", [Forbidden, NotAuthenticated]) +async def test_auth_error_triggers_reauth( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + side_effect: type[Exception], +) -> None: + """Test that an auth error during coordinator refresh triggers reauth.""" + mock_config_entry.add_to_hass(hass) + + with patch( + "homeassistant.components.huum.coordinator.Huum.status", + side_effect=side_effect, + ): + await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + assert any( + mock_config_entry.async_get_active_flows(hass, {config_entries.SOURCE_REAUTH}) + )