mirror of
https://github.com/home-assistant/core.git
synced 2025-07-10 06:47:09 +00:00
Improve error handling and add exception translations for NextDNS integration (#141005)
* Add exception translations * Coverage * Add missing auth_error * Coverage * Use async_start_reauth * Fix test * Remove method placeholder
This commit is contained in:
parent
a338205b73
commit
53f1dd8adf
@ -36,6 +36,7 @@ from .const import (
|
|||||||
ATTR_SETTINGS,
|
ATTR_SETTINGS,
|
||||||
ATTR_STATUS,
|
ATTR_STATUS,
|
||||||
CONF_PROFILE_ID,
|
CONF_PROFILE_ID,
|
||||||
|
DOMAIN,
|
||||||
UPDATE_INTERVAL_ANALYTICS,
|
UPDATE_INTERVAL_ANALYTICS,
|
||||||
UPDATE_INTERVAL_CONNECTION,
|
UPDATE_INTERVAL_CONNECTION,
|
||||||
UPDATE_INTERVAL_SETTINGS,
|
UPDATE_INTERVAL_SETTINGS,
|
||||||
@ -88,9 +89,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: NextDnsConfigEntry) -> b
|
|||||||
try:
|
try:
|
||||||
nextdns = await NextDns.create(websession, api_key)
|
nextdns = await NextDns.create(websession, api_key)
|
||||||
except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err:
|
except (ApiError, ClientConnectorError, RetryError, TimeoutError) as err:
|
||||||
raise ConfigEntryNotReady from err
|
raise ConfigEntryNotReady(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="cannot_connect",
|
||||||
|
translation_placeholders={
|
||||||
|
"entry": entry.title,
|
||||||
|
"error": repr(err),
|
||||||
|
},
|
||||||
|
) from err
|
||||||
except InvalidApiKeyError as err:
|
except InvalidApiKeyError as err:
|
||||||
raise ConfigEntryAuthFailed from err
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="auth_error",
|
||||||
|
translation_placeholders={"entry": entry.title},
|
||||||
|
) from err
|
||||||
|
|
||||||
tasks = []
|
tasks = []
|
||||||
coordinators = {}
|
coordinators = {}
|
||||||
|
@ -2,15 +2,19 @@
|
|||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from nextdns import AnalyticsStatus
|
from aiohttp import ClientError
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
from nextdns import AnalyticsStatus, ApiError, InvalidApiKeyError
|
||||||
|
|
||||||
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
from homeassistant.components.button import ButtonEntity, ButtonEntityDescription
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import NextDnsConfigEntry
|
from . import NextDnsConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import NextDnsUpdateCoordinator
|
from .coordinator import NextDnsUpdateCoordinator
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
@ -53,4 +57,21 @@ class NextDnsButton(
|
|||||||
|
|
||||||
async def async_press(self) -> None:
|
async def async_press(self) -> None:
|
||||||
"""Trigger cleaning logs."""
|
"""Trigger cleaning logs."""
|
||||||
await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id)
|
try:
|
||||||
|
await self.coordinator.nextdns.clear_logs(self.coordinator.profile_id)
|
||||||
|
except (
|
||||||
|
ApiError,
|
||||||
|
ClientConnectorError,
|
||||||
|
TimeoutError,
|
||||||
|
ClientError,
|
||||||
|
) as err:
|
||||||
|
raise HomeAssistantError(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="method_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"entity": self.entity_id,
|
||||||
|
"error": repr(err),
|
||||||
|
},
|
||||||
|
) from err
|
||||||
|
except InvalidApiKeyError:
|
||||||
|
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||||
|
@ -79,9 +79,20 @@ class NextDnsUpdateCoordinator(DataUpdateCoordinator[CoordinatorDataT]):
|
|||||||
ClientConnectorError,
|
ClientConnectorError,
|
||||||
RetryError,
|
RetryError,
|
||||||
) as err:
|
) as err:
|
||||||
raise UpdateFailed(err) from err
|
raise UpdateFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="update_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"entry": self.config_entry.title,
|
||||||
|
"error": repr(err),
|
||||||
|
},
|
||||||
|
) from err
|
||||||
except InvalidApiKeyError as err:
|
except InvalidApiKeyError as err:
|
||||||
raise ConfigEntryAuthFailed from err
|
raise ConfigEntryAuthFailed(
|
||||||
|
translation_domain=DOMAIN,
|
||||||
|
translation_key="auth_error",
|
||||||
|
translation_placeholders={"entry": self.config_entry.title},
|
||||||
|
) from err
|
||||||
|
|
||||||
async def _async_update_data_internal(self) -> CoordinatorDataT:
|
async def _async_update_data_internal(self) -> CoordinatorDataT:
|
||||||
"""Update data via library."""
|
"""Update data via library."""
|
||||||
|
@ -359,5 +359,19 @@
|
|||||||
"name": "Force YouTube restricted mode"
|
"name": "Force YouTube restricted mode"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
"exceptions": {
|
||||||
|
"auth_error": {
|
||||||
|
"message": "Authentication failed for {entry}, please update your API key"
|
||||||
|
},
|
||||||
|
"cannot_connect": {
|
||||||
|
"message": "An error occurred while connecting to the NextDNS API for {entry}: {error}"
|
||||||
|
},
|
||||||
|
"method_error": {
|
||||||
|
"message": "An error occurred while calling the NextDNS API method for {entity}: {error}"
|
||||||
|
},
|
||||||
|
"update_error": {
|
||||||
|
"message": "An error occurred while retrieving data from the NextDNS API for {entry}: {error}"
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -8,7 +8,7 @@ from typing import Any
|
|||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
from nextdns import ApiError, Settings
|
from nextdns import ApiError, InvalidApiKeyError, Settings
|
||||||
|
|
||||||
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
from homeassistant.components.switch import SwitchEntity, SwitchEntityDescription
|
||||||
from homeassistant.const import EntityCategory
|
from homeassistant.const import EntityCategory
|
||||||
@ -18,6 +18,7 @@ from homeassistant.helpers.entity_platform import AddConfigEntryEntitiesCallback
|
|||||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||||
|
|
||||||
from . import NextDnsConfigEntry
|
from . import NextDnsConfigEntry
|
||||||
|
from .const import DOMAIN
|
||||||
from .coordinator import NextDnsUpdateCoordinator
|
from .coordinator import NextDnsUpdateCoordinator
|
||||||
|
|
||||||
PARALLEL_UPDATES = 1
|
PARALLEL_UPDATES = 1
|
||||||
@ -582,9 +583,16 @@ class NextDnsSwitch(
|
|||||||
ClientError,
|
ClientError,
|
||||||
) as err:
|
) as err:
|
||||||
raise HomeAssistantError(
|
raise HomeAssistantError(
|
||||||
"NextDNS API returned an error calling set_setting for"
|
translation_domain=DOMAIN,
|
||||||
f" {self.entity_id}: {err}"
|
translation_key="method_error",
|
||||||
|
translation_placeholders={
|
||||||
|
"entity": self.entity_id,
|
||||||
|
"error": repr(err),
|
||||||
|
},
|
||||||
) from err
|
) from err
|
||||||
|
except InvalidApiKeyError:
|
||||||
|
self.coordinator.config_entry.async_start_reauth(self.hass)
|
||||||
|
return
|
||||||
|
|
||||||
if result:
|
if result:
|
||||||
self._attr_is_on = new_state
|
self._attr_is_on = new_state
|
||||||
|
@ -1,12 +1,19 @@
|
|||||||
"""Test button of NextDNS integration."""
|
"""Test button of NextDNS integration."""
|
||||||
|
|
||||||
from unittest.mock import patch
|
from unittest.mock import Mock, patch
|
||||||
|
|
||||||
|
from aiohttp import ClientError
|
||||||
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
|
from nextdns import ApiError, InvalidApiKeyError
|
||||||
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
|
|
||||||
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN
|
from homeassistant.components.button import DOMAIN as BUTTON_DOMAIN, SERVICE_PRESS
|
||||||
|
from homeassistant.components.nextdns.const import DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
from homeassistant.const import ATTR_ENTITY_ID, Platform
|
||||||
from homeassistant.core import HomeAssistant
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import HomeAssistantError
|
||||||
from homeassistant.helpers import entity_registry as er
|
from homeassistant.helpers import entity_registry as er
|
||||||
from homeassistant.util import dt as dt_util
|
from homeassistant.util import dt as dt_util
|
||||||
|
|
||||||
@ -36,7 +43,7 @@ async def test_button_press(hass: HomeAssistant) -> None:
|
|||||||
):
|
):
|
||||||
await hass.services.async_call(
|
await hass.services.async_call(
|
||||||
BUTTON_DOMAIN,
|
BUTTON_DOMAIN,
|
||||||
"press",
|
SERVICE_PRESS,
|
||||||
{ATTR_ENTITY_ID: "button.fake_profile_clear_logs"},
|
{ATTR_ENTITY_ID: "button.fake_profile_clear_logs"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
@ -47,3 +54,60 @@ async def test_button_press(hass: HomeAssistant) -> None:
|
|||||||
state = hass.states.get("button.fake_profile_clear_logs")
|
state = hass.states.get("button.fake_profile_clear_logs")
|
||||||
assert state
|
assert state
|
||||||
assert state.state == now.isoformat()
|
assert state.state == now.isoformat()
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"exc",
|
||||||
|
[
|
||||||
|
ApiError(Mock()),
|
||||||
|
TimeoutError,
|
||||||
|
ClientConnectorError(Mock(), Mock()),
|
||||||
|
ClientError,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
async def test_button_failure(hass: HomeAssistant, exc: Exception) -> None:
|
||||||
|
"""Tests that the press action throws HomeAssistantError."""
|
||||||
|
await init_integration(hass)
|
||||||
|
|
||||||
|
with (
|
||||||
|
patch("homeassistant.components.nextdns.NextDns.clear_logs", side_effect=exc),
|
||||||
|
pytest.raises(
|
||||||
|
HomeAssistantError,
|
||||||
|
match="An error occurred while calling the NextDNS API method for button.fake_profile_clear_logs",
|
||||||
|
),
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{ATTR_ENTITY_ID: "button.fake_profile_clear_logs"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_button_auth_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Tests that the press action starts re-auth flow."""
|
||||||
|
entry = await init_integration(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nextdns.NextDns.clear_logs",
|
||||||
|
side_effect=InvalidApiKeyError,
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
BUTTON_DOMAIN,
|
||||||
|
SERVICE_PRESS,
|
||||||
|
{ATTR_ENTITY_ID: "button.fake_profile_clear_logs"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
|
||||||
|
flow = flows[0]
|
||||||
|
assert flow.get("step_id") == "reauth_confirm"
|
||||||
|
assert flow.get("handler") == DOMAIN
|
||||||
|
|
||||||
|
assert "context" in flow
|
||||||
|
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||||
|
assert flow["context"].get("entry_id") == entry.entry_id
|
||||||
|
@ -5,12 +5,14 @@ from unittest.mock import Mock, patch
|
|||||||
|
|
||||||
from aiohttp import ClientError
|
from aiohttp import ClientError
|
||||||
from aiohttp.client_exceptions import ClientConnectorError
|
from aiohttp.client_exceptions import ClientConnectorError
|
||||||
from nextdns import ApiError
|
from nextdns import ApiError, InvalidApiKeyError
|
||||||
import pytest
|
import pytest
|
||||||
from syrupy import SnapshotAssertion
|
from syrupy import SnapshotAssertion
|
||||||
from tenacity import RetryError
|
from tenacity import RetryError
|
||||||
|
|
||||||
|
from homeassistant.components.nextdns.const import DOMAIN
|
||||||
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
|
||||||
|
from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState
|
||||||
from homeassistant.const import (
|
from homeassistant.const import (
|
||||||
ATTR_ENTITY_ID,
|
ATTR_ENTITY_ID,
|
||||||
SERVICE_TURN_OFF,
|
SERVICE_TURN_OFF,
|
||||||
@ -158,3 +160,32 @@ async def test_switch_failure(hass: HomeAssistant, exc: Exception) -> None:
|
|||||||
{ATTR_ENTITY_ID: "switch.fake_profile_block_page"},
|
{ATTR_ENTITY_ID: "switch.fake_profile_block_page"},
|
||||||
blocking=True,
|
blocking=True,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_switch_auth_error(hass: HomeAssistant) -> None:
|
||||||
|
"""Tests that the turn on/off action starts re-auth flow."""
|
||||||
|
entry = await init_integration(hass)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.nextdns.NextDns.set_setting",
|
||||||
|
side_effect=InvalidApiKeyError,
|
||||||
|
):
|
||||||
|
await hass.services.async_call(
|
||||||
|
SWITCH_DOMAIN,
|
||||||
|
SERVICE_TURN_ON,
|
||||||
|
{ATTR_ENTITY_ID: "switch.fake_profile_block_page"},
|
||||||
|
blocking=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert entry.state is ConfigEntryState.LOADED
|
||||||
|
|
||||||
|
flows = hass.config_entries.flow.async_progress()
|
||||||
|
assert len(flows) == 1
|
||||||
|
|
||||||
|
flow = flows[0]
|
||||||
|
assert flow.get("step_id") == "reauth_confirm"
|
||||||
|
assert flow.get("handler") == DOMAIN
|
||||||
|
|
||||||
|
assert "context" in flow
|
||||||
|
assert flow["context"].get("source") == SOURCE_REAUTH
|
||||||
|
assert flow["context"].get("entry_id") == entry.entry_id
|
||||||
|
Loading…
x
Reference in New Issue
Block a user