1
0
mirror of https://github.com/home-assistant/core.git synced 2025-12-27 14:31:13 +00:00

Support reconfigure flow in NextDNS integration (#154936)

This commit is contained in:
Maciej Bieniek
2025-10-25 19:13:58 +02:00
committed by GitHub
parent 750f06327a
commit 7bceaf74be
5 changed files with 212 additions and 20 deletions

View File

@@ -14,6 +14,7 @@ import voluptuous as vol
from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
from homeassistant.const import CONF_API_KEY, CONF_PROFILE_NAME
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.helpers.aiohttp_client import async_get_clientsession
from .const import CONF_PROFILE_ID, DOMAIN
@@ -23,11 +24,40 @@ AUTH_SCHEMA = vol.Schema({vol.Required(CONF_API_KEY): str})
_LOGGER = logging.getLogger(__name__)
async def async_init_nextdns(hass: HomeAssistant, api_key: str) -> NextDns:
"""Check if credentials are valid."""
async def async_init_nextdns(
hass: HomeAssistant, api_key: str, profile_id: str | None = None
) -> NextDns:
"""Check if credentials and profile_id are valid."""
websession = async_get_clientsession(hass)
return await NextDns.create(websession, api_key)
nextdns = await NextDns.create(websession, api_key)
if profile_id:
if not any(profile.id == profile_id for profile in nextdns.profiles):
raise ProfileNotAvailable
return nextdns
async def async_validate_new_api_key(
hass: HomeAssistant, user_input: dict[str, Any], profile_id: str
) -> dict[str, str]:
"""Validate the new API key during reconfiguration or reauth."""
errors: dict[str, str] = {}
try:
await async_init_nextdns(hass, user_input[CONF_API_KEY], profile_id)
except InvalidApiKeyError:
errors["base"] = "invalid_api_key"
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
errors["base"] = "cannot_connect"
except ProfileNotAvailable:
errors["base"] = "profile_not_available"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
return errors
class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
@@ -107,20 +137,19 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
) -> ConfigFlowResult:
"""Dialog that informs the user that reauth is required."""
errors: dict[str, str] = {}
entry = self._get_reauth_entry()
if user_input is not None:
try:
await async_init_nextdns(self.hass, user_input[CONF_API_KEY])
except InvalidApiKeyError:
errors["base"] = "invalid_api_key"
except (ApiError, ClientConnectorError, RetryError, TimeoutError):
errors["base"] = "cannot_connect"
except Exception:
_LOGGER.exception("Unexpected exception")
errors["base"] = "unknown"
else:
errors = await async_validate_new_api_key(
self.hass, user_input, entry.data[CONF_PROFILE_ID]
)
if errors.get("base") == "profile_not_available":
return self.async_abort(reason="profile_not_available")
if not errors:
return self.async_update_reload_and_abort(
self._get_reauth_entry(), data_updates=user_input
entry,
data_updates=user_input,
)
return self.async_show_form(
@@ -128,3 +157,33 @@ class NextDnsFlowHandler(ConfigFlow, domain=DOMAIN):
data_schema=AUTH_SCHEMA,
errors=errors,
)
async def async_step_reconfigure(
self, user_input: dict[str, Any] | None = None
) -> ConfigFlowResult:
"""Handle a reconfiguration flow initialized by the user."""
errors: dict[str, str] = {}
entry = self._get_reconfigure_entry()
if user_input is not None:
errors = await async_validate_new_api_key(
self.hass, user_input, entry.data[CONF_PROFILE_ID]
)
if errors.get("base") == "profile_not_available":
return self.async_abort(reason="profile_not_available")
if not errors:
return self.async_update_reload_and_abort(
entry,
data_updates=user_input,
)
return self.async_show_form(
step_id="reconfigure",
data_schema=AUTH_SCHEMA,
errors=errors,
)
class ProfileNotAvailable(HomeAssistantError):
"""Error to indicate that the profile is not available after reconfig/reauth."""

View File

@@ -7,6 +7,6 @@
"integration_type": "service",
"iot_class": "cloud_polling",
"loggers": ["nextdns"],
"quality_scale": "silver",
"quality_scale": "platinum",
"requirements": ["nextdns==4.1.0"]
}

View File

@@ -68,9 +68,7 @@ rules:
entity-translations: done
exception-translations: done
icon-translations: done
reconfiguration-flow:
status: todo
comment: Allow API key to be changed in the re-configure flow.
reconfiguration-flow: done
repair-issues:
status: exempt
comment: This integration doesn't have any cases where raising an issue is needed.

View File

@@ -24,6 +24,14 @@
"data_description": {
"api_key": "[%key:component::nextdns::config::step::user::data_description::api_key%]"
}
},
"reconfigure": {
"data": {
"api_key": "[%key:common::config_flow::data::api_key%]"
},
"data_description": {
"api_key": "[%key:component::nextdns::config::step::user::data_description::api_key%]"
}
}
},
"error": {
@@ -33,7 +41,9 @@
},
"abort": {
"already_configured": "This NextDNS profile is already configured.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]"
"profile_not_available": "The configured NextDNS profile is no longer available in your account. Remove the configuration and configure the integration again.",
"reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]",
"reconfigure_successful": "[%key:common::config_flow::abort::reconfigure_successful%]"
}
},
"system_health": {

View File

@@ -2,7 +2,7 @@
from unittest.mock import AsyncMock
from nextdns import ApiError, InvalidApiKeyError
from nextdns import ApiError, InvalidApiKeyError, ProfileInfo
import pytest
from tenacity import RetryError
@@ -154,6 +154,32 @@ async def test_reauth_successful(
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new_api_key"
async def test_reauth_no_profile(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nextdns_client: AsyncMock,
) -> None:
"""Test reauthentication flow when the profile is no longer available."""
await init_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reauth_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reauth_confirm"
mock_nextdns_client.profiles = [
ProfileInfo(id="abcd098", fingerprint="abcd098", name="New Profile")
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new_api_key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "profile_not_available"
@pytest.mark.parametrize(
@@ -196,6 +222,105 @@ async def test_reauth_errors(
result["flow_id"],
user_input={CONF_API_KEY: "new_api_key"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reauth_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new_api_key"
async def test_reconfigure_flow(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nextdns_client: AsyncMock,
) -> None:
"""Test starting a reconfigure flow."""
await init_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new_api_key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new_api_key"
@pytest.mark.parametrize(
("exc", "base_error"),
[
(ApiError("API Error"), "cannot_connect"),
(InvalidApiKeyError, "invalid_api_key"),
(RetryError("Retry Error"), "cannot_connect"),
(TimeoutError, "cannot_connect"),
(ValueError, "unknown"),
],
)
async def test_reconfiguration_errors(
hass: HomeAssistant,
exc: Exception,
base_error: str,
mock_config_entry: MockConfigEntry,
mock_nextdns_client: AsyncMock,
mock_nextdns: AsyncMock,
) -> None:
"""Test reconfigure flow with errors."""
await init_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
mock_nextdns.create.side_effect = exc
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new_api_key"},
)
assert result["errors"] == {"base": base_error}
mock_nextdns.create.side_effect = None
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new_api_key"},
)
await hass.async_block_till_done()
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "reconfigure_successful"
assert mock_config_entry.data[CONF_API_KEY] == "new_api_key"
async def test_reconfigure_flow_no_profile(
hass: HomeAssistant,
mock_config_entry: MockConfigEntry,
mock_nextdns_client: AsyncMock,
) -> None:
"""Test reconfigure flow when the profile is no longer available."""
await init_integration(hass, mock_config_entry)
result = await mock_config_entry.start_reconfigure_flow(hass)
assert result["type"] is FlowResultType.FORM
assert result["step_id"] == "reconfigure"
mock_nextdns_client.profiles = [
ProfileInfo(id="abcd098", fingerprint="abcd098", name="New Profile")
]
result = await hass.config_entries.flow.async_configure(
result["flow_id"],
user_input={CONF_API_KEY: "new_api_key"},
)
assert result["type"] is FlowResultType.ABORT
assert result["reason"] == "profile_not_available"