Refresh the bond token if it has changed and available (#57583)

This commit is contained in:
J. Nick Koston 2021-10-12 18:39:46 -10:00 committed by GitHub
parent 282300f3e4
commit 2adb9a8bec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 118 additions and 18 deletions

View File

@ -1,11 +1,17 @@
"""The Bond integration.""" """The Bond integration."""
from asyncio import TimeoutError as AsyncIOTimeoutError from asyncio import TimeoutError as AsyncIOTimeoutError
import logging
from aiohttp import ClientError, ClientTimeout from aiohttp import ClientError, ClientResponseError, ClientTimeout
from bond_api import Bond, BPUPSubscriptions, start_bpup from bond_api import Bond, BPUPSubscriptions, start_bpup
from homeassistant.config_entries import ConfigEntry from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST, EVENT_HOMEASSISTANT_STOP from homeassistant.const import (
CONF_ACCESS_TOKEN,
CONF_HOST,
EVENT_HOMEASSISTANT_STOP,
HTTP_UNAUTHORIZED,
)
from homeassistant.core import Event, HomeAssistant, callback from homeassistant.core import Event, HomeAssistant, callback
from homeassistant.exceptions import ConfigEntryNotReady from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers import device_registry as dr from homeassistant.helpers import device_registry as dr
@ -19,6 +25,8 @@ PLATFORMS = ["cover", "fan", "light", "switch"]
_API_TIMEOUT = SLOW_UPDATE_WARNING - 1 _API_TIMEOUT = SLOW_UPDATE_WARNING - 1
_STOP_CANCEL = "stop_cancel" _STOP_CANCEL = "stop_cancel"
_LOGGER = logging.getLogger(__name__)
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
"""Set up Bond from a config entry.""" """Set up Bond from a config entry."""
@ -35,6 +43,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
hub = BondHub(bond) hub = BondHub(bond)
try: try:
await hub.setup() await hub.setup()
except ClientResponseError as ex:
if ex.status == HTTP_UNAUTHORIZED:
_LOGGER.error("Bond token no longer valid: %s", ex)
return False
raise ConfigEntryNotReady from ex
except (ClientError, AsyncIOTimeoutError, OSError) as error: except (ClientError, AsyncIOTimeoutError, OSError) as error:
raise ConfigEntryNotReady from error raise ConfigEntryNotReady from error

View File

@ -9,6 +9,7 @@ from bond_api import Bond
import voluptuous as vol import voluptuous as vol
from homeassistant import config_entries, exceptions from homeassistant import config_entries, exceptions
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import ( from homeassistant.const import (
CONF_ACCESS_TOKEN, CONF_ACCESS_TOKEN,
CONF_HOST, CONF_HOST,
@ -16,7 +17,7 @@ from homeassistant.const import (
HTTP_UNAUTHORIZED, HTTP_UNAUTHORIZED,
) )
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResult from homeassistant.data_entry_flow import AbortFlow, FlowResult
from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.helpers.aiohttp_client import async_get_clientsession
from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.helpers.typing import DiscoveryInfoType
@ -33,6 +34,16 @@ DISCOVERY_SCHEMA = vol.Schema({vol.Required(CONF_ACCESS_TOKEN): str})
TOKEN_SCHEMA = vol.Schema({}) TOKEN_SCHEMA = vol.Schema({})
async def async_get_token(hass: HomeAssistant, host: str) -> str | None:
"""Try to fetch the token from the bond device."""
bond = Bond(host, "", session=async_get_clientsession(hass))
try:
response: dict[str, str] = await bond.token()
except ClientConnectionError:
return None
return response.get("token")
async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]: async def _validate_input(hass: HomeAssistant, data: dict[str, Any]) -> tuple[str, str]:
"""Validate the user input allows us to connect.""" """Validate the user input allows us to connect."""
@ -75,16 +86,8 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
online longer then the allowed setup period, and we will online longer then the allowed setup period, and we will
instead ask them to manually enter the token. instead ask them to manually enter the token.
""" """
bond = Bond( host = self._discovered[CONF_HOST]
self._discovered[CONF_HOST], "", session=async_get_clientsession(self.hass) if not (token := await async_get_token(self.hass, host)):
)
try:
response = await bond.token()
except ClientConnectionError:
return
token = response.get("token")
if token is None:
return return
self._discovered[CONF_ACCESS_TOKEN] = token self._discovered[CONF_ACCESS_TOKEN] = token
@ -99,7 +102,21 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
host: str = discovery_info[CONF_HOST] host: str = discovery_info[CONF_HOST]
bond_id = name.partition(".")[0] bond_id = name.partition(".")[0]
await self.async_set_unique_id(bond_id) await self.async_set_unique_id(bond_id)
self._abort_if_unique_id_configured({CONF_HOST: host}) for entry in self._async_current_entries():
if entry.unique_id != bond_id:
continue
updates = {CONF_HOST: host}
if entry.state == ConfigEntryState.SETUP_ERROR and (
token := await async_get_token(self.hass, host)
):
updates[CONF_ACCESS_TOKEN] = token
self.hass.config_entries.async_update_entry(
entry, data={**entry.data, **updates}
)
self.hass.async_create_task(
self.hass.config_entries.async_reload(entry.entry_id)
)
raise AbortFlow("already_configured")
self._discovered = {CONF_HOST: host, CONF_NAME: bond_id} self._discovered = {CONF_HOST: host, CONF_NAME: bond_id}
await self._async_try_automatic_configure() await self._async_try_automatic_configure()

View File

@ -2,12 +2,13 @@
from __future__ import annotations from __future__ import annotations
from typing import Any from typing import Any
from unittest.mock import Mock, patch from unittest.mock import MagicMock, Mock, patch
from aiohttp import ClientConnectionError, ClientResponseError from aiohttp import ClientConnectionError, ClientResponseError
from homeassistant import config_entries, core from homeassistant import config_entries, core
from homeassistant.components.bond.const import DOMAIN from homeassistant.components.bond.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST from homeassistant.const import CONF_ACCESS_TOKEN, CONF_HOST
from .common import ( from .common import (
@ -308,6 +309,49 @@ async def test_zeroconf_already_configured(hass: core.HomeAssistant):
assert len(mock_setup_entry.mock_calls) == 0 assert len(mock_setup_entry.mock_calls) == 0
async def test_zeroconf_already_configured_refresh_token(hass: core.HomeAssistant):
"""Test starting a flow from zeroconf when already configured and the token is out of date."""
entry2 = MockConfigEntry(
domain=DOMAIN,
unique_id="not-the-same-bond-id",
data={CONF_HOST: "stored-host", CONF_ACCESS_TOKEN: "correct-token"},
)
entry2.add_to_hass(hass)
entry = MockConfigEntry(
domain=DOMAIN,
unique_id="already-registered-bond-id",
data={CONF_HOST: "stored-host", CONF_ACCESS_TOKEN: "incorrect-token"},
)
entry.add_to_hass(hass)
with patch_bond_version(
side_effect=ClientResponseError(MagicMock(), MagicMock(), status=401)
):
await hass.config_entries.async_setup(entry.entry_id)
assert entry.state is ConfigEntryState.SETUP_ERROR
with _patch_async_setup_entry() as mock_setup_entry, patch_bond_token(
return_value={"token": "discovered-token"}
):
result = await hass.config_entries.flow.async_init(
DOMAIN,
context={"source": config_entries.SOURCE_ZEROCONF},
data={
"name": "already-registered-bond-id.some-other-tail-info",
"host": "updated-host",
},
)
await hass.async_block_till_done()
assert result["type"] == "abort"
assert result["reason"] == "already_configured"
assert entry.data["host"] == "updated-host"
assert entry.data[CONF_ACCESS_TOKEN] == "discovered-token"
# entry2 should not get changed
assert entry2.data[CONF_ACCESS_TOKEN] == "correct-token"
assert len(mock_setup_entry.mock_calls) == 1
async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant): async def test_zeroconf_form_unexpected_error(hass: core.HomeAssistant):
"""Test we handle unexpected error gracefully.""" """Test we handle unexpected error gracefully."""
await _help_test_form_unexpected_error( await _help_test_form_unexpected_error(

View File

@ -1,8 +1,10 @@
"""Tests for the Bond module.""" """Tests for the Bond module."""
from unittest.mock import Mock import asyncio
from unittest.mock import MagicMock, Mock
from aiohttp import ClientConnectionError, ClientResponseError from aiohttp import ClientConnectionError, ClientResponseError
from bond_api import DeviceType from bond_api import DeviceType
import pytest
from homeassistant.components.bond.const import DOMAIN from homeassistant.components.bond.const import DOMAIN
from homeassistant.config_entries import ConfigEntryState from homeassistant.config_entries import ConfigEntryState
@ -33,7 +35,16 @@ async def test_async_setup_no_domain_config(hass: HomeAssistant):
assert result is True assert result is True
async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant): @pytest.mark.parametrize(
"exc",
[
ClientConnectionError,
ClientResponseError(MagicMock(), MagicMock(), status=404),
asyncio.TimeoutError,
OSError,
],
)
async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant, exc: Exception):
"""Test that it throws ConfigEntryNotReady when exception occurs during setup.""" """Test that it throws ConfigEntryNotReady when exception occurs during setup."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(
domain=DOMAIN, domain=DOMAIN,
@ -41,11 +52,26 @@ async def test_async_setup_raises_entry_not_ready(hass: HomeAssistant):
) )
config_entry.add_to_hass(hass) config_entry.add_to_hass(hass)
with patch_bond_version(side_effect=ClientConnectionError()): with patch_bond_version(side_effect=exc):
await hass.config_entries.async_setup(config_entry.entry_id) await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_RETRY assert config_entry.state is ConfigEntryState.SETUP_RETRY
async def test_async_setup_raises_fails_if_auth_fails(hass: HomeAssistant):
"""Test that setup fails if auth fails during setup."""
config_entry = MockConfigEntry(
domain=DOMAIN,
data={CONF_HOST: "some host", CONF_ACCESS_TOKEN: "test-token"},
)
config_entry.add_to_hass(hass)
with patch_bond_version(
side_effect=ClientResponseError(MagicMock(), MagicMock(), status=401)
):
await hass.config_entries.async_setup(config_entry.entry_id)
assert config_entry.state is ConfigEntryState.SETUP_ERROR
async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAssistant): async def test_async_setup_entry_sets_up_hub_and_supported_domains(hass: HomeAssistant):
"""Test that configuring entry sets up cover domain.""" """Test that configuring entry sets up cover domain."""
config_entry = MockConfigEntry( config_entry = MockConfigEntry(