Skip to content

Commit 0976301

Browse files
Add reconfigure flow to nederlandse_spoorwegen (#155412)
Co-authored-by: Josef Zweck <[email protected]>
1 parent aa5b970 commit 0976301

File tree

3 files changed

+204
-12
lines changed

3 files changed

+204
-12
lines changed

homeassistant/components/nederlandse_spoorwegen/config_flow.py

Lines changed: 66 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -49,23 +49,52 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
4949
VERSION = 1
5050
MINOR_VERSION = 1
5151

52+
async def _validate_api_key(self, api_key: str) -> dict[str, str]:
53+
"""Validate the API key by testing connection to NS API.
54+
55+
Returns a dict of errors, empty if validation successful.
56+
"""
57+
errors: dict[str, str] = {}
58+
client = NSAPI(api_key)
59+
try:
60+
await self.hass.async_add_executor_job(client.get_stations)
61+
except HTTPError:
62+
errors["base"] = "invalid_auth"
63+
except (RequestsConnectionError, Timeout):
64+
errors["base"] = "cannot_connect"
65+
except Exception:
66+
_LOGGER.exception("Unexpected exception validating API key")
67+
errors["base"] = "unknown"
68+
return errors
69+
70+
def _is_api_key_already_configured(
71+
self, api_key: str, exclude_entry_id: str | None = None
72+
) -> dict[str, str]:
73+
"""Check if the API key is already configured in another entry.
74+
75+
Args:
76+
api_key: The API key to check.
77+
exclude_entry_id: Optional entry ID to exclude from the check.
78+
79+
Returns:
80+
A dict of errors, empty if not already configured.
81+
"""
82+
for entry in self._async_current_entries():
83+
if (
84+
entry.entry_id != exclude_entry_id
85+
and entry.data.get(CONF_API_KEY) == api_key
86+
):
87+
return {"base": "already_configured"}
88+
return {}
89+
5290
async def async_step_user(
5391
self, user_input: dict[str, Any] | None = None
5492
) -> ConfigFlowResult:
5593
"""Handle the initial step of the config flow (API key)."""
5694
errors: dict[str, str] = {}
5795
if user_input is not None:
5896
self._async_abort_entries_match(user_input)
59-
client = NSAPI(user_input[CONF_API_KEY])
60-
try:
61-
await self.hass.async_add_executor_job(client.get_stations)
62-
except HTTPError:
63-
errors["base"] = "invalid_auth"
64-
except (RequestsConnectionError, Timeout):
65-
errors["base"] = "cannot_connect"
66-
except Exception:
67-
_LOGGER.exception("Unexpected exception validating API key")
68-
errors["base"] = "unknown"
97+
errors = await self._validate_api_key(user_input[CONF_API_KEY])
6998
if not errors:
7099
return self.async_create_entry(
71100
title=INTEGRATION_TITLE,
@@ -77,6 +106,33 @@ async def async_step_user(
77106
errors=errors,
78107
)
79108

109+
async def async_step_reconfigure(
110+
self, user_input: dict[str, Any] | None = None
111+
) -> ConfigFlowResult:
112+
"""Handle reconfiguration to update the API key from the UI."""
113+
errors: dict[str, str] = {}
114+
115+
reconfigure_entry = self._get_reconfigure_entry()
116+
117+
if user_input is not None:
118+
# Check if this API key is already used by another entry
119+
errors = self._is_api_key_already_configured(
120+
user_input[CONF_API_KEY], exclude_entry_id=reconfigure_entry.entry_id
121+
)
122+
123+
if not errors:
124+
errors = await self._validate_api_key(user_input[CONF_API_KEY])
125+
if not errors:
126+
return self.async_update_reload_and_abort(
127+
reconfigure_entry,
128+
data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
129+
)
130+
return self.async_show_form(
131+
step_id="reconfigure",
132+
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
133+
errors=errors,
134+
)
135+
80136
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
81137
"""Handle import from YAML configuration."""
82138
self._async_abort_entries_match({CONF_API_KEY: import_data[CONF_API_KEY]})

homeassistant/components/nederlandse_spoorwegen/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_service%]"
4+
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
5+
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
56
},
67
"error": {
8+
"already_configured": "This API key is already configured for another entry.",
79
"cannot_connect": "Could not connect to NS API. Check your API key.",
810
"invalid_auth": "[%key:common::config_flow::error::invalid_api_key%]",
911
"unknown": "[%key:common::config_flow::error::unknown%]"
1012
},
1113
"step": {
14+
"reconfigure": {
15+
"data": {
16+
"api_key": "[%key:common::config_flow::data::api_key%]"
17+
},
18+
"data_description": {
19+
"api_key": "[%key:component::nederlandse_spoorwegen::config::step::user::data_description::api_key%]"
20+
},
21+
"description": "Update your Nederlandse Spoorwegen API key."
22+
},
1223
"user": {
1324
"data": {
1425
"api_key": "[%key:common::config_flow::data::api_key%]"

tests/components/nederlandse_spoorwegen/test_config_flow.py

Lines changed: 126 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
CONF_VIA,
1616
DOMAIN,
1717
)
18-
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
18+
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_RECONFIGURE, SOURCE_USER
1919
from homeassistant.const import CONF_API_KEY, CONF_NAME
2020
from homeassistant.core import HomeAssistant
2121
from homeassistant.data_entry_flow import FlowResultType
@@ -331,3 +331,128 @@ async def test_import_flow_exceptions(
331331
)
332332
assert result["type"] is FlowResultType.ABORT
333333
assert result["reason"] == expected_error
334+
335+
336+
async def test_reconfigure_success(
337+
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_config_entry: MockConfigEntry
338+
) -> None:
339+
"""Test successfully reconfiguring (updating) the API key."""
340+
new_key = "new_api_key_123456"
341+
342+
mock_config_entry.add_to_hass(hass)
343+
344+
# Start reconfigure flow
345+
result = await hass.config_entries.flow.async_init(
346+
DOMAIN,
347+
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
348+
)
349+
350+
assert result["type"] is FlowResultType.FORM
351+
assert result["step_id"] == "reconfigure"
352+
353+
# Submit new API key, mock_nsapi.get_stations returns OK by default
354+
result2 = await hass.config_entries.flow.async_configure(
355+
result["flow_id"], user_input={CONF_API_KEY: new_key}
356+
)
357+
358+
assert result2["type"] is FlowResultType.ABORT
359+
assert result2["reason"] == "reconfigure_successful"
360+
361+
# Entry should be updated with the new API key
362+
assert mock_config_entry.data[CONF_API_KEY] == new_key
363+
364+
365+
@pytest.mark.parametrize(
366+
("exception", "expected_error"),
367+
[
368+
(HTTPError("Invalid API key"), "invalid_auth"),
369+
(Timeout("Cannot connect"), "cannot_connect"),
370+
(RequestsConnectionError("Cannot connect"), "cannot_connect"),
371+
(Exception("Unexpected error"), "unknown"),
372+
],
373+
)
374+
async def test_reconfigure_errors(
375+
hass: HomeAssistant,
376+
mock_nsapi: AsyncMock,
377+
mock_config_entry: MockConfigEntry,
378+
exception: Exception,
379+
expected_error: str,
380+
) -> None:
381+
"""Test reconfigure flow error handling (invalid auth and cannot connect)."""
382+
mock_config_entry.add_to_hass(hass)
383+
384+
# First present the form
385+
result = await hass.config_entries.flow.async_init(
386+
DOMAIN,
387+
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
388+
)
389+
assert result["type"] is FlowResultType.FORM
390+
assert result["step_id"] == "reconfigure"
391+
392+
# Make get_stations raise the requested exception
393+
mock_nsapi.get_stations.side_effect = exception
394+
395+
result2 = await hass.config_entries.flow.async_configure(
396+
result["flow_id"], user_input={CONF_API_KEY: "bad_key"}
397+
)
398+
399+
assert result2["type"] is FlowResultType.FORM
400+
assert result2["errors"] == {"base": expected_error}
401+
402+
# Clear side effect and submit valid API key to complete the flow
403+
mock_nsapi.get_stations.side_effect = None
404+
405+
result3 = await hass.config_entries.flow.async_configure(
406+
result["flow_id"], user_input={CONF_API_KEY: "new_valid_key"}
407+
)
408+
409+
assert result3["type"] is FlowResultType.ABORT
410+
assert result3["reason"] == "reconfigure_successful"
411+
assert mock_config_entry.data[CONF_API_KEY] == "new_valid_key"
412+
413+
414+
async def test_reconfigure_already_configured(
415+
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_config_entry: MockConfigEntry
416+
) -> None:
417+
"""Test reconfiguring with an API key that's already used by another entry."""
418+
# Add first entry
419+
mock_config_entry.add_to_hass(hass)
420+
421+
# Create and add second entry with different API key
422+
second_entry = MockConfigEntry(
423+
domain=DOMAIN,
424+
title="NS Integration 2",
425+
data={CONF_API_KEY: "another_api_key_456"},
426+
unique_id="second_entry",
427+
)
428+
second_entry.add_to_hass(hass)
429+
430+
# Start reconfigure flow for the first entry
431+
result = await hass.config_entries.flow.async_init(
432+
DOMAIN,
433+
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
434+
)
435+
436+
assert result["type"] is FlowResultType.FORM
437+
assert result["step_id"] == "reconfigure"
438+
439+
# Try to reconfigure to use the API key from the second entry
440+
result2 = await hass.config_entries.flow.async_configure(
441+
result["flow_id"], user_input={CONF_API_KEY: "another_api_key_456"}
442+
)
443+
444+
# Should show error that it's already configured
445+
assert result2["type"] is FlowResultType.FORM
446+
assert result2["errors"] == {"base": "already_configured"}
447+
448+
# Verify the original entry was not changed
449+
assert mock_config_entry.data[CONF_API_KEY] == API_KEY
450+
451+
# Now submit a valid unique API key to complete the flow
452+
result3 = await hass.config_entries.flow.async_configure(
453+
result["flow_id"], user_input={CONF_API_KEY: "new_unique_key_789"}
454+
)
455+
456+
assert result3["type"] is FlowResultType.ABORT
457+
assert result3["reason"] == "reconfigure_successful"
458+
assert mock_config_entry.data[CONF_API_KEY] == "new_unique_key_789"

0 commit comments

Comments
 (0)