Skip to content

Commit a374c7e

Browse files
authored
Add reauth flow to Ohme (#133275)
* Add reauth flow to ohme * Reuse config flow user step for reauth * Tidying up * Add common _validate_account method for reauth and user config flow steps * Add reauth fail test
1 parent 9cdc366 commit a374c7e

File tree

6 files changed

+150
-13
lines changed

6 files changed

+150
-13
lines changed

homeassistant/components/ohme/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
from homeassistant.config_entries import ConfigEntry
88
from homeassistant.const import CONF_EMAIL, CONF_PASSWORD
99
from homeassistant.core import HomeAssistant
10-
from homeassistant.exceptions import ConfigEntryError, ConfigEntryNotReady
10+
from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady
1111

1212
from .const import DOMAIN, PLATFORMS
1313
from .coordinator import OhmeAdvancedSettingsCoordinator, OhmeChargeSessionCoordinator
@@ -36,7 +36,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: OhmeConfigEntry) -> bool
3636
translation_key="device_info_failed", translation_domain=DOMAIN
3737
)
3838
except AuthException as e:
39-
raise ConfigEntryError(
39+
raise ConfigEntryAuthFailed(
4040
translation_key="auth_failed", translation_domain=DOMAIN
4141
) from e
4242
except ApiException as e:

homeassistant/components/ohme/config_flow.py

Lines changed: 60 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Config flow for ohme integration."""
22

3+
from collections.abc import Mapping
34
from typing import Any
45

56
from ohme import ApiException, AuthException, OhmeApiClient
@@ -32,6 +33,17 @@
3233
}
3334
)
3435

36+
REAUTH_SCHEMA = vol.Schema(
37+
{
38+
vol.Required(CONF_PASSWORD): TextSelector(
39+
TextSelectorConfig(
40+
type=TextSelectorType.PASSWORD,
41+
autocomplete="current-password",
42+
),
43+
),
44+
}
45+
)
46+
3547

3648
class OhmeConfigFlow(ConfigFlow, domain=DOMAIN):
3749
"""Config flow."""
@@ -46,14 +58,9 @@ async def async_step_user(
4658
if user_input is not None:
4759
self._async_abort_entries_match({CONF_EMAIL: user_input[CONF_EMAIL]})
4860

49-
instance = OhmeApiClient(user_input[CONF_EMAIL], user_input[CONF_PASSWORD])
50-
try:
51-
await instance.async_login()
52-
except AuthException:
53-
errors["base"] = "invalid_auth"
54-
except ApiException:
55-
errors["base"] = "unknown"
56-
61+
errors = await self._validate_account(
62+
user_input[CONF_EMAIL], user_input[CONF_PASSWORD]
63+
)
5764
if not errors:
5865
return self.async_create_entry(
5966
title=user_input[CONF_EMAIL], data=user_input
@@ -62,3 +69,48 @@ async def async_step_user(
6269
return self.async_show_form(
6370
step_id="user", data_schema=USER_SCHEMA, errors=errors
6471
)
72+
73+
async def async_step_reauth(
74+
self, entry_data: Mapping[str, Any]
75+
) -> ConfigFlowResult:
76+
"""Handle re-authentication."""
77+
return await self.async_step_reauth_confirm()
78+
79+
async def async_step_reauth_confirm(
80+
self, user_input: dict[str, Any] | None = None
81+
) -> ConfigFlowResult:
82+
"""Handle re-authentication confirmation."""
83+
errors: dict[str, str] = {}
84+
reauth_entry = self._get_reauth_entry()
85+
if user_input is not None:
86+
errors = await self._validate_account(
87+
reauth_entry.data[CONF_EMAIL],
88+
user_input[CONF_PASSWORD],
89+
)
90+
if not errors:
91+
return self.async_update_reload_and_abort(
92+
reauth_entry,
93+
data_updates=user_input,
94+
)
95+
return self.async_show_form(
96+
step_id="reauth_confirm",
97+
data_schema=REAUTH_SCHEMA,
98+
description_placeholders={"email": reauth_entry.data[CONF_EMAIL]},
99+
errors=errors,
100+
)
101+
102+
async def _validate_account(self, email: str, password: str) -> dict[str, str]:
103+
"""Validate Ohme account and return dict of errors."""
104+
errors: dict[str, str] = {}
105+
client = OhmeApiClient(
106+
email,
107+
password,
108+
)
109+
try:
110+
await client.async_login()
111+
except AuthException:
112+
errors["base"] = "invalid_auth"
113+
except ApiException:
114+
errors["base"] = "unknown"
115+
116+
return errors

homeassistant/components/ohme/manifest.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,6 @@
66
"documentation": "https://www.home-assistant.io/integrations/ohme/",
77
"integration_type": "device",
88
"iot_class": "cloud_polling",
9-
"quality_scale": "bronze",
9+
"quality_scale": "silver",
1010
"requirements": ["ohme==1.1.1"]
1111
}

homeassistant/components/ohme/quality_scale.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ rules:
4040
integration-owner: done
4141
log-when-unavailable: done
4242
parallel-updates: done
43-
reauthentication-flow: todo
43+
reauthentication-flow: done
4444
test-coverage: done
4545

4646
# Gold

homeassistant/components/ohme/strings.json

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,14 +11,25 @@
1111
"email": "Enter the email address associated with your Ohme account.",
1212
"password": "Enter the password for your Ohme account"
1313
}
14+
},
15+
"reauth_confirm": {
16+
"description": "Please update your password for {email}",
17+
"title": "[%key:common::config_flow::title::reauth%]",
18+
"data": {
19+
"password": "[%key:common::config_flow::data::password%]"
20+
},
21+
"data_description": {
22+
"password": "Enter the password for your Ohme account"
23+
}
1424
}
1525
},
1626
"error": {
1727
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
1828
"unknown": "[%key:common::config_flow::error::unknown%]"
1929
},
2030
"abort": {
21-
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
31+
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]",
32+
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
2233
}
2334
},
2435
"entity": {

tests/components/ohme/test_config_flow.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,3 +108,77 @@ async def test_already_configured(
108108

109109
assert result["type"] is FlowResultType.ABORT
110110
assert result["reason"] == "already_configured"
111+
112+
113+
async def test_reauth_form(hass: HomeAssistant, mock_client: MagicMock) -> None:
114+
"""Test reauth form."""
115+
entry = MockConfigEntry(
116+
domain=DOMAIN,
117+
data={
118+
CONF_EMAIL: "[email protected]",
119+
CONF_PASSWORD: "hunter1",
120+
},
121+
)
122+
entry.add_to_hass(hass)
123+
result = await entry.start_reauth_flow(hass)
124+
assert result["type"] is FlowResultType.FORM
125+
assert result["step_id"] == "reauth_confirm"
126+
127+
assert not result["errors"]
128+
result = await hass.config_entries.flow.async_configure(
129+
result["flow_id"],
130+
{CONF_PASSWORD: "hunter2"},
131+
)
132+
await hass.async_block_till_done()
133+
assert result["type"] is FlowResultType.ABORT
134+
assert result["reason"] == "reauth_successful"
135+
136+
137+
@pytest.mark.parametrize(
138+
("test_exception", "expected_error"),
139+
[(AuthException, "invalid_auth"), (ApiException, "unknown")],
140+
)
141+
async def test_reauth_fail(
142+
hass: HomeAssistant,
143+
mock_client: MagicMock,
144+
test_exception: Exception,
145+
expected_error: str,
146+
) -> None:
147+
"""Test reauth errors."""
148+
149+
entry = MockConfigEntry(
150+
domain=DOMAIN,
151+
data={
152+
CONF_EMAIL: "[email protected]",
153+
CONF_PASSWORD: "hunter1",
154+
},
155+
)
156+
entry.add_to_hass(hass)
157+
158+
# Initial form load
159+
result = await entry.start_reauth_flow(hass)
160+
161+
assert result["step_id"] == "reauth_confirm"
162+
assert result["type"] is FlowResultType.FORM
163+
assert not result["errors"]
164+
165+
# Failed login
166+
mock_client.async_login.side_effect = test_exception
167+
168+
result = await hass.config_entries.flow.async_configure(
169+
result["flow_id"],
170+
{CONF_PASSWORD: "hunter1"},
171+
)
172+
assert result["type"] is FlowResultType.FORM
173+
assert result["errors"] == {"base": expected_error}
174+
175+
# End with success
176+
mock_client.async_login.side_effect = None
177+
178+
result = await hass.config_entries.flow.async_configure(
179+
result["flow_id"],
180+
{CONF_PASSWORD: "hunter2"},
181+
)
182+
await hass.async_block_till_done()
183+
assert result["type"] is FlowResultType.ABORT
184+
assert result["reason"] == "reauth_successful"

0 commit comments

Comments
 (0)