mirror of
https://github.com/home-assistant/core.git
synced 2026-02-15 07:36:16 +00:00
Add reconfigure flow to nederlandse_spoorwegen (#155412)
Co-authored-by: Josef Zweck <josef@zweck.dev>
This commit is contained in:
@@ -49,6 +49,44 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
VERSION = 1
|
||||
MINOR_VERSION = 1
|
||||
|
||||
async def _validate_api_key(self, api_key: str) -> dict[str, str]:
|
||||
"""Validate the API key by testing connection to NS API.
|
||||
|
||||
Returns a dict of errors, empty if validation successful.
|
||||
"""
|
||||
errors: dict[str, str] = {}
|
||||
client = NSAPI(api_key)
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.get_stations)
|
||||
except HTTPError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (RequestsConnectionError, Timeout):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception validating API key")
|
||||
errors["base"] = "unknown"
|
||||
return errors
|
||||
|
||||
def _is_api_key_already_configured(
|
||||
self, api_key: str, exclude_entry_id: str | None = None
|
||||
) -> dict[str, str]:
|
||||
"""Check if the API key is already configured in another entry.
|
||||
|
||||
Args:
|
||||
api_key: The API key to check.
|
||||
exclude_entry_id: Optional entry ID to exclude from the check.
|
||||
|
||||
Returns:
|
||||
A dict of errors, empty if not already configured.
|
||||
"""
|
||||
for entry in self._async_current_entries():
|
||||
if (
|
||||
entry.entry_id != exclude_entry_id
|
||||
and entry.data.get(CONF_API_KEY) == api_key
|
||||
):
|
||||
return {"base": "already_configured"}
|
||||
return {}
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
@@ -56,16 +94,7 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
self._async_abort_entries_match(user_input)
|
||||
client = NSAPI(user_input[CONF_API_KEY])
|
||||
try:
|
||||
await self.hass.async_add_executor_job(client.get_stations)
|
||||
except HTTPError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except (RequestsConnectionError, Timeout):
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception validating API key")
|
||||
errors["base"] = "unknown"
|
||||
errors = await self._validate_api_key(user_input[CONF_API_KEY])
|
||||
if not errors:
|
||||
return self.async_create_entry(
|
||||
title=INTEGRATION_TITLE,
|
||||
@@ -77,6 +106,33 @@ class NSConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_reconfigure(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle reconfiguration to update the API key from the UI."""
|
||||
errors: dict[str, str] = {}
|
||||
|
||||
reconfigure_entry = self._get_reconfigure_entry()
|
||||
|
||||
if user_input is not None:
|
||||
# Check if this API key is already used by another entry
|
||||
errors = self._is_api_key_already_configured(
|
||||
user_input[CONF_API_KEY], exclude_entry_id=reconfigure_entry.entry_id
|
||||
)
|
||||
|
||||
if not errors:
|
||||
errors = await self._validate_api_key(user_input[CONF_API_KEY])
|
||||
if not errors:
|
||||
return self.async_update_reload_and_abort(
|
||||
reconfigure_entry,
|
||||
data_updates={CONF_API_KEY: user_input[CONF_API_KEY]},
|
||||
)
|
||||
return self.async_show_form(
|
||||
step_id="reconfigure",
|
||||
data_schema=vol.Schema({vol.Required(CONF_API_KEY): str}),
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_data: dict[str, Any]) -> ConfigFlowResult:
|
||||
"""Handle import from YAML configuration."""
|
||||
self._async_abort_entries_match({CONF_API_KEY: import_data[CONF_API_KEY]})
|
||||
|
||||
@@ -1,14 +1,25 @@
|
||||
{
|
||||
"config": {
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]",
|
||||
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
|
||||
},
|
||||
"error": {
|
||||
"already_configured": "This API key is already configured for another entry.",
|
||||
"cannot_connect": "Could not connect to NS API. Check your API key.",
|
||||
"invalid_auth": "[%key:common::config_flow::error::invalid_api_key%]",
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"step": {
|
||||
"reconfigure": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::nederlandse_spoorwegen::config::step::user::data_description::api_key%]"
|
||||
},
|
||||
"description": "Update your Nederlandse Spoorwegen API key."
|
||||
},
|
||||
"user": {
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]"
|
||||
|
||||
@@ -15,7 +15,7 @@ from homeassistant.components.nederlandse_spoorwegen.const import (
|
||||
CONF_VIA,
|
||||
DOMAIN,
|
||||
)
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_RECONFIGURE, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_NAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
@@ -331,3 +331,128 @@ async def test_import_flow_exceptions(
|
||||
)
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == expected_error
|
||||
|
||||
|
||||
async def test_reconfigure_success(
|
||||
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test successfully reconfiguring (updating) the API key."""
|
||||
new_key = "new_api_key_123456"
|
||||
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Start reconfigure flow
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
# Submit new API key, mock_nsapi.get_stations returns OK by default
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: new_key}
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.ABORT
|
||||
assert result2["reason"] == "reconfigure_successful"
|
||||
|
||||
# Entry should be updated with the new API key
|
||||
assert mock_config_entry.data[CONF_API_KEY] == new_key
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "expected_error"),
|
||||
[
|
||||
(HTTPError("Invalid API key"), "invalid_auth"),
|
||||
(Timeout("Cannot connect"), "cannot_connect"),
|
||||
(RequestsConnectionError("Cannot connect"), "cannot_connect"),
|
||||
(Exception("Unexpected error"), "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_reconfigure_errors(
|
||||
hass: HomeAssistant,
|
||||
mock_nsapi: AsyncMock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
exception: Exception,
|
||||
expected_error: str,
|
||||
) -> None:
|
||||
"""Test reconfigure flow error handling (invalid auth and cannot connect)."""
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# First present the form
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
)
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
# Make get_stations raise the requested exception
|
||||
mock_nsapi.get_stations.side_effect = exception
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "bad_key"}
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": expected_error}
|
||||
|
||||
# Clear side effect and submit valid API key to complete the flow
|
||||
mock_nsapi.get_stations.side_effect = None
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "new_valid_key"}
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
assert result3["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "new_valid_key"
|
||||
|
||||
|
||||
async def test_reconfigure_already_configured(
|
||||
hass: HomeAssistant, mock_nsapi: AsyncMock, mock_config_entry: MockConfigEntry
|
||||
) -> None:
|
||||
"""Test reconfiguring with an API key that's already used by another entry."""
|
||||
# Add first entry
|
||||
mock_config_entry.add_to_hass(hass)
|
||||
|
||||
# Create and add second entry with different API key
|
||||
second_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
title="NS Integration 2",
|
||||
data={CONF_API_KEY: "another_api_key_456"},
|
||||
unique_id="second_entry",
|
||||
)
|
||||
second_entry.add_to_hass(hass)
|
||||
|
||||
# Start reconfigure flow for the first entry
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_RECONFIGURE, "entry_id": mock_config_entry.entry_id},
|
||||
)
|
||||
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "reconfigure"
|
||||
|
||||
# Try to reconfigure to use the API key from the second entry
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "another_api_key_456"}
|
||||
)
|
||||
|
||||
# Should show error that it's already configured
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "already_configured"}
|
||||
|
||||
# Verify the original entry was not changed
|
||||
assert mock_config_entry.data[CONF_API_KEY] == API_KEY
|
||||
|
||||
# Now submit a valid unique API key to complete the flow
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"], user_input={CONF_API_KEY: "new_unique_key_789"}
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.ABORT
|
||||
assert result3["reason"] == "reconfigure_successful"
|
||||
assert mock_config_entry.data[CONF_API_KEY] == "new_unique_key_789"
|
||||
|
||||
Reference in New Issue
Block a user