Fix account link no internet on startup (#154579)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Will Moss
2025-11-05 15:23:20 +01:00
committed by GitHub
co-authored by Martin Hjelmare
parent 6dc655c3b4
commit 9e3eb20a04
5 changed files with 147 additions and 10 deletions
@@ -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)
+2 -2
View File
@@ -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
)