mirror of
https://github.com/home-assistant/core.git
synced 2026-08-24 10:13:52 -05:00
Fix account link no internet on startup (#154579)
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
co-authored by
Martin Hjelmare
parent
6dc655c3b4
commit
9e3eb20a04
@@ -71,8 +71,11 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]:
|
||||
services = await account_link.async_fetch_available_services(
|
||||
hass.data[DATA_CLOUD]
|
||||
)
|
||||
except (aiohttp.ClientError, TimeoutError):
|
||||
return []
|
||||
except (aiohttp.ClientError, TimeoutError) as err:
|
||||
raise config_entry_oauth2_flow.ImplementationUnavailableError(
|
||||
"Cannot provide OAuth2 implementation for cloud services. "
|
||||
"Failed to fetch from account link server."
|
||||
) from err
|
||||
|
||||
hass.data[DATA_SERVICES] = services
|
||||
|
||||
|
||||
@@ -29,6 +29,7 @@ from yarl import URL
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import HomeAssistant, callback
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import async_get_application_credentials
|
||||
from homeassistant.util.hass_dict import HassKey
|
||||
|
||||
@@ -61,6 +62,10 @@ OAUTH_AUTHORIZE_URL_TIMEOUT_SEC = 30
|
||||
OAUTH_TOKEN_TIMEOUT_SEC = 30
|
||||
|
||||
|
||||
class ImplementationUnavailableError(HomeAssistantError):
|
||||
"""Raised when an underlying implementation is unavailable."""
|
||||
|
||||
|
||||
@callback
|
||||
def async_get_redirect_uri(hass: HomeAssistant) -> str:
|
||||
"""Return the redirect uri."""
|
||||
@@ -563,9 +568,16 @@ async def async_get_implementations(
|
||||
return registered
|
||||
|
||||
registered = dict(registered)
|
||||
exceptions = []
|
||||
for get_impl in list(hass.data[DATA_PROVIDERS].values()):
|
||||
for impl in await get_impl(hass, domain):
|
||||
registered[impl.domain] = impl
|
||||
try:
|
||||
for impl in await get_impl(hass, domain):
|
||||
registered[impl.domain] = impl
|
||||
except ImplementationUnavailableError as err:
|
||||
exceptions.append(err)
|
||||
|
||||
if not registered and exceptions:
|
||||
raise ImplementationUnavailableError(*exceptions)
|
||||
|
||||
return registered
|
||||
|
||||
|
||||
@@ -5,7 +5,11 @@ from __future__ import annotations
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
|
||||
from homeassistant.helpers.config_entry_oauth2_flow import (
|
||||
ImplementationUnavailableError,
|
||||
)
|
||||
|
||||
from . import api
|
||||
|
||||
@@ -21,11 +25,16 @@ type New_NameConfigEntry = ConfigEntry[api.AsyncConfigEntryAuth]
|
||||
# # TODO Update entry annotation
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: New_NameConfigEntry) -> bool:
|
||||
"""Set up NEW_NAME from a config entry."""
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
try:
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, entry
|
||||
)
|
||||
)
|
||||
)
|
||||
except ImplementationUnavailableError as err:
|
||||
raise ConfigEntryNotReady(
|
||||
"OAuth2 implementation temporarily unavailable, will retry"
|
||||
) from err
|
||||
|
||||
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
|
||||
|
||||
|
||||
@@ -177,9 +177,9 @@ async def test_get_services_error(hass: HomeAssistant) -> None:
|
||||
"hass_nabucasa.account_link.async_fetch_available_services",
|
||||
side_effect=TimeoutError,
|
||||
),
|
||||
pytest.raises(config_entry_oauth2_flow.ImplementationUnavailableError),
|
||||
):
|
||||
assert await account_link._get_services(hass) == []
|
||||
assert account_link.DATA_SERVICES not in hass.data
|
||||
await account_link._get_services(hass)
|
||||
|
||||
|
||||
@pytest.mark.usefixtures("current_request_with_host")
|
||||
|
||||
@@ -1137,3 +1137,116 @@ def test_compute_code_challenge_invalid_code_verifier(code_verifier: str) -> Non
|
||||
config_entry_oauth2_flow.LocalOAuth2ImplementationWithPkce.compute_code_challenge(
|
||||
code_verifier
|
||||
)
|
||||
|
||||
|
||||
async def test_async_get_config_entry_implementation_with_failing_provider_and_succeeding_provider(
|
||||
hass: HomeAssistant,
|
||||
local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation,
|
||||
) -> None:
|
||||
"""Test async_get_config_entry_implementation when one provider fails but another succeeds."""
|
||||
|
||||
async def failing_cloud_provider(
|
||||
_hass: HomeAssistant, _domain: str
|
||||
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
|
||||
"""Provider that raises an exception."""
|
||||
raise config_entry_oauth2_flow.ImplementationUnavailableError
|
||||
|
||||
async def successful_local_provider(
|
||||
_hass: HomeAssistant, _domain: str
|
||||
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
|
||||
"""Provider that returns implementations."""
|
||||
return [local_impl]
|
||||
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
hass, "cloud", failing_cloud_provider
|
||||
)
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
hass, "application_credentials", successful_local_provider
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=TEST_DOMAIN,
|
||||
data={
|
||||
"auth_implementation": local_impl.domain,
|
||||
},
|
||||
)
|
||||
|
||||
# This should succeed and return the local implementation
|
||||
# even though the failing cloud provider raised an exception.
|
||||
implementation = (
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, config_entry
|
||||
)
|
||||
)
|
||||
assert implementation is local_impl
|
||||
|
||||
|
||||
async def test_async_get_config_entry_implementation_with_failing_provider(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_get_config_entry_implementation when one provider fails and the other is empty."""
|
||||
|
||||
async def failing_cloud_provider(
|
||||
_hass: HomeAssistant, _domain: str
|
||||
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
|
||||
"""Provider that raises an exception."""
|
||||
raise config_entry_oauth2_flow.ImplementationUnavailableError
|
||||
|
||||
async def empty_local_provider(
|
||||
_hass: HomeAssistant, _domain: str
|
||||
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
|
||||
"""Provider that returns implementations."""
|
||||
return []
|
||||
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
hass, "cloud", failing_cloud_provider
|
||||
)
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
hass, "application_credentials", empty_local_provider
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=TEST_DOMAIN,
|
||||
data={
|
||||
"auth_implementation": TEST_DOMAIN,
|
||||
},
|
||||
)
|
||||
|
||||
# This should fail since the local provider returned an empty list
|
||||
# and the cloud provider raised an exception.
|
||||
with pytest.raises(config_entry_oauth2_flow.ImplementationUnavailableError):
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, config_entry
|
||||
)
|
||||
|
||||
|
||||
async def test_async_get_config_entry_implementation_missing_provider(
|
||||
hass: HomeAssistant,
|
||||
) -> None:
|
||||
"""Test async_get_config_entry_implementation when both providers are empty."""
|
||||
|
||||
async def empty_provider(
|
||||
_hass: HomeAssistant, _domain: str
|
||||
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
|
||||
"""Provider that returns implementations."""
|
||||
return []
|
||||
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
hass, "cloud", empty_provider
|
||||
)
|
||||
config_entry_oauth2_flow.async_add_implementation_provider(
|
||||
hass, "application_credentials", empty_provider
|
||||
)
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=TEST_DOMAIN,
|
||||
data={
|
||||
"auth_implementation": TEST_DOMAIN,
|
||||
},
|
||||
)
|
||||
|
||||
# This should fail since both providers are empty.
|
||||
with pytest.raises(ValueError, match="Implementation not available"):
|
||||
await config_entry_oauth2_flow.async_get_config_entry_implementation(
|
||||
hass, config_entry
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user