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:
@@ -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."""
|
||||
|
||||
@@ -7,6 +7,6 @@
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["nextdns"],
|
||||
"quality_scale": "silver",
|
||||
"quality_scale": "platinum",
|
||||
"requirements": ["nextdns==4.1.0"]
|
||||
}
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in New Issue
Block a user