Skip to content

Commit 66b1728

Browse files
authored
Implement reauth for Huum integration (#165971)
1 parent d11668b commit 66b1728

File tree

6 files changed

+162
-5
lines changed

6 files changed

+162
-5
lines changed

homeassistant/components/huum/config_flow.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from __future__ import annotations
44

5+
from collections.abc import Mapping
56
import logging
67
from typing import Any
78

@@ -57,3 +58,48 @@ async def async_step_user(
5758
return self.async_show_form(
5859
step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors
5960
)
61+
62+
async def async_step_reauth(
63+
self, entry_data: Mapping[str, Any]
64+
) -> ConfigFlowResult:
65+
"""Handle reauthentication upon an API authentication error."""
66+
return await self.async_step_reauth_confirm()
67+
68+
async def async_step_reauth_confirm(
69+
self, user_input: dict[str, Any] | None = None
70+
) -> ConfigFlowResult:
71+
"""Confirm reauthentication dialog."""
72+
errors: dict[str, str] = {}
73+
reauth_entry = self._get_reauth_entry()
74+
75+
if user_input is not None:
76+
huum = Huum(
77+
reauth_entry.data[CONF_USERNAME],
78+
user_input[CONF_PASSWORD],
79+
session=async_get_clientsession(self.hass),
80+
)
81+
try:
82+
await huum.status()
83+
except Forbidden, NotAuthenticated:
84+
errors["base"] = "invalid_auth"
85+
except Exception:
86+
_LOGGER.exception("Unknown error")
87+
errors["base"] = "unknown"
88+
else:
89+
return self.async_update_reload_and_abort(
90+
reauth_entry,
91+
data_updates={CONF_PASSWORD: user_input[CONF_PASSWORD]},
92+
)
93+
94+
return self.async_show_form(
95+
step_id="reauth_confirm",
96+
data_schema=vol.Schema(
97+
{
98+
vol.Required(CONF_PASSWORD): str,
99+
}
100+
),
101+
description_placeholders={
102+
"username": reauth_entry.data[CONF_USERNAME],
103+
},
104+
errors=errors,
105+
)

homeassistant/components/huum/coordinator.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,9 @@
1212
from homeassistant.config_entries import ConfigEntry
1313
from homeassistant.const import CONF_PASSWORD, CONF_USERNAME
1414
from homeassistant.core import HomeAssistant
15+
from homeassistant.exceptions import ConfigEntryAuthFailed
1516
from homeassistant.helpers.aiohttp_client import async_get_clientsession
16-
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
17+
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
1718

1819
from .const import DOMAIN
1920

@@ -54,6 +55,6 @@ async def _async_update_data(self) -> HuumStatusResponse:
5455
try:
5556
return await self.huum.status()
5657
except (Forbidden, NotAuthenticated) as err:
57-
raise UpdateFailed(
58+
raise ConfigEntryAuthFailed(
5859
"Could not log in to Huum with given credentials"
5960
) from err

homeassistant/components/huum/quality_scale.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ rules:
3838
integration-owner: done
3939
log-when-unavailable: done
4040
parallel-updates: done
41-
reauthentication-flow: todo
41+
reauthentication-flow: done
4242
test-coverage:
4343
status: todo
4444
comment: |

homeassistant/components/huum/strings.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,25 @@
11
{
22
"config": {
33
"abort": {
4-
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
4+
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
5+
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
56
},
67
"error": {
78
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
89
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
910
"unknown": "[%key:common::config_flow::error::unknown%]"
1011
},
1112
"step": {
13+
"reauth_confirm": {
14+
"data": {
15+
"password": "[%key:common::config_flow::data::password%]"
16+
},
17+
"data_description": {
18+
"password": "[%key:component::huum::config::step::user::data_description::password%]"
19+
},
20+
"description": "The authentication for {username} is no longer valid. Please enter the current password.",
21+
"title": "[%key:common::config_flow::title::reauth%]"
22+
},
1223
"user": {
1324
"data": {
1425
"password": "[%key:common::config_flow::data::password%]",

tests/components/huum/test_config_flow.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -115,3 +115,76 @@ async def test_huum_errors(
115115
},
116116
)
117117
assert result["type"] is FlowResultType.CREATE_ENTRY
118+
119+
120+
async def test_reauth_flow(
121+
hass: HomeAssistant,
122+
mock_huum: AsyncMock,
123+
mock_config_entry: MockConfigEntry,
124+
) -> None:
125+
"""Test reauthentication flow succeeds with valid credentials."""
126+
mock_config_entry.add_to_hass(hass)
127+
128+
result = await mock_config_entry.start_reauth_flow(hass)
129+
130+
assert result["type"] is FlowResultType.FORM
131+
assert result["step_id"] == "reauth_confirm"
132+
assert result["errors"] == {}
133+
134+
result = await hass.config_entries.flow.async_configure(
135+
result["flow_id"],
136+
{CONF_PASSWORD: "new_password"},
137+
)
138+
await hass.async_block_till_done()
139+
140+
assert result["type"] is FlowResultType.ABORT
141+
assert result["reason"] == "reauth_successful"
142+
assert mock_config_entry.data[CONF_USERNAME] == TEST_USERNAME
143+
assert mock_config_entry.data[CONF_PASSWORD] == "new_password"
144+
145+
146+
@pytest.mark.parametrize(
147+
(
148+
"raises",
149+
"error_base",
150+
),
151+
[
152+
(Exception, "unknown"),
153+
(Forbidden, "invalid_auth"),
154+
],
155+
)
156+
async def test_reauth_errors(
157+
hass: HomeAssistant,
158+
mock_huum: AsyncMock,
159+
mock_config_entry: MockConfigEntry,
160+
raises: Exception,
161+
error_base: str,
162+
) -> None:
163+
"""Test reauthentication flow handles errors and recovers."""
164+
mock_config_entry.add_to_hass(hass)
165+
166+
result = await mock_config_entry.start_reauth_flow(hass)
167+
168+
with patch(
169+
"homeassistant.components.huum.config_flow.Huum.status",
170+
side_effect=raises,
171+
):
172+
result = await hass.config_entries.flow.async_configure(
173+
result["flow_id"],
174+
{CONF_PASSWORD: "wrong_password"},
175+
)
176+
177+
assert result["type"] is FlowResultType.FORM
178+
assert result["errors"] == {"base": error_base}
179+
180+
# Recover with valid credentials
181+
result = await hass.config_entries.flow.async_configure(
182+
result["flow_id"],
183+
{CONF_PASSWORD: "new_password"},
184+
)
185+
await hass.async_block_till_done()
186+
187+
assert result["type"] is FlowResultType.ABORT
188+
assert result["reason"] == "reauth_successful"
189+
assert mock_config_entry.data[CONF_USERNAME] == TEST_USERNAME
190+
assert mock_config_entry.data[CONF_PASSWORD] == "new_password"

tests/components/huum/test_init.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
"""Tests for the Huum __init__."""
22

3-
from unittest.mock import AsyncMock
3+
from unittest.mock import AsyncMock, patch
44

5+
from huum.exceptions import Forbidden, NotAuthenticated
6+
import pytest
7+
8+
from homeassistant import config_entries
59
from homeassistant.components.huum.const import DOMAIN
610
from homeassistant.config_entries import ConfigEntryState
711
from homeassistant.const import Platform
@@ -25,3 +29,25 @@ async def test_loading_and_unloading_config_entry(
2529
await hass.async_block_till_done()
2630

2731
assert mock_config_entry.state is ConfigEntryState.NOT_LOADED
32+
33+
34+
@pytest.mark.parametrize("side_effect", [Forbidden, NotAuthenticated])
35+
async def test_auth_error_triggers_reauth(
36+
hass: HomeAssistant,
37+
mock_config_entry: MockConfigEntry,
38+
side_effect: type[Exception],
39+
) -> None:
40+
"""Test that an auth error during coordinator refresh triggers reauth."""
41+
mock_config_entry.add_to_hass(hass)
42+
43+
with patch(
44+
"homeassistant.components.huum.coordinator.Huum.status",
45+
side_effect=side_effect,
46+
):
47+
await hass.config_entries.async_setup(mock_config_entry.entry_id)
48+
await hass.async_block_till_done()
49+
50+
assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR
51+
assert any(
52+
mock_config_entry.async_get_active_flows(hass, {config_entries.SOURCE_REAUTH})
53+
)

0 commit comments

Comments
 (0)