Skip to content

Commit 9e3eb20

Browse files
Fix account link no internet on startup (#154579)
Co-authored-by: Martin Hjelmare <[email protected]>
1 parent 6dc655c commit 9e3eb20

File tree

5 files changed

+147
-10
lines changed

5 files changed

+147
-10
lines changed

homeassistant/components/cloud/account_link.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,11 @@ async def _get_services(hass: HomeAssistant) -> list[dict[str, Any]]:
7171
services = await account_link.async_fetch_available_services(
7272
hass.data[DATA_CLOUD]
7373
)
74-
except (aiohttp.ClientError, TimeoutError):
75-
return []
74+
except (aiohttp.ClientError, TimeoutError) as err:
75+
raise config_entry_oauth2_flow.ImplementationUnavailableError(
76+
"Cannot provide OAuth2 implementation for cloud services. "
77+
"Failed to fetch from account link server."
78+
) from err
7679

7780
hass.data[DATA_SERVICES] = services
7881

homeassistant/helpers/config_entry_oauth2_flow.py

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929

3030
from homeassistant import config_entries
3131
from homeassistant.core import HomeAssistant, callback
32+
from homeassistant.exceptions import HomeAssistantError
3233
from homeassistant.loader import async_get_application_credentials
3334
from homeassistant.util.hass_dict import HassKey
3435

@@ -61,6 +62,10 @@
6162
OAUTH_TOKEN_TIMEOUT_SEC = 30
6263

6364

65+
class ImplementationUnavailableError(HomeAssistantError):
66+
"""Raised when an underlying implementation is unavailable."""
67+
68+
6469
@callback
6570
def async_get_redirect_uri(hass: HomeAssistant) -> str:
6671
"""Return the redirect uri."""
@@ -563,9 +568,16 @@ async def async_get_implementations(
563568
return registered
564569

565570
registered = dict(registered)
571+
exceptions = []
566572
for get_impl in list(hass.data[DATA_PROVIDERS].values()):
567-
for impl in await get_impl(hass, domain):
568-
registered[impl.domain] = impl
573+
try:
574+
for impl in await get_impl(hass, domain):
575+
registered[impl.domain] = impl
576+
except ImplementationUnavailableError as err:
577+
exceptions.append(err)
578+
579+
if not registered and exceptions:
580+
raise ImplementationUnavailableError(*exceptions)
569581

570582
return registered
571583

script/scaffold/templates/config_flow_oauth2/integration/__init__.py

Lines changed: 13 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,11 @@
55
from homeassistant.config_entries import ConfigEntry
66
from homeassistant.const import Platform
77
from homeassistant.core import HomeAssistant
8+
from homeassistant.exceptions import ConfigEntryNotReady
89
from homeassistant.helpers import aiohttp_client, config_entry_oauth2_flow
10+
from homeassistant.helpers.config_entry_oauth2_flow import (
11+
ImplementationUnavailableError,
12+
)
913

1014
from . import api
1115

@@ -21,11 +25,16 @@
2125
# # TODO Update entry annotation
2226
async def async_setup_entry(hass: HomeAssistant, entry: New_NameConfigEntry) -> bool:
2327
"""Set up NEW_NAME from a config entry."""
24-
implementation = (
25-
await config_entry_oauth2_flow.async_get_config_entry_implementation(
26-
hass, entry
28+
try:
29+
implementation = (
30+
await config_entry_oauth2_flow.async_get_config_entry_implementation(
31+
hass, entry
32+
)
2733
)
28-
)
34+
except ImplementationUnavailableError as err:
35+
raise ConfigEntryNotReady(
36+
"OAuth2 implementation temporarily unavailable, will retry"
37+
) from err
2938

3039
session = config_entry_oauth2_flow.OAuth2Session(hass, entry, implementation)
3140

tests/components/cloud/test_account_link.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -177,9 +177,9 @@ async def test_get_services_error(hass: HomeAssistant) -> None:
177177
"hass_nabucasa.account_link.async_fetch_available_services",
178178
side_effect=TimeoutError,
179179
),
180+
pytest.raises(config_entry_oauth2_flow.ImplementationUnavailableError),
180181
):
181-
assert await account_link._get_services(hass) == []
182-
assert account_link.DATA_SERVICES not in hass.data
182+
await account_link._get_services(hass)
183183

184184

185185
@pytest.mark.usefixtures("current_request_with_host")

tests/helpers/test_config_entry_oauth2_flow.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1137,3 +1137,116 @@ def test_compute_code_challenge_invalid_code_verifier(code_verifier: str) -> Non
11371137
config_entry_oauth2_flow.LocalOAuth2ImplementationWithPkce.compute_code_challenge(
11381138
code_verifier
11391139
)
1140+
1141+
1142+
async def test_async_get_config_entry_implementation_with_failing_provider_and_succeeding_provider(
1143+
hass: HomeAssistant,
1144+
local_impl: config_entry_oauth2_flow.LocalOAuth2Implementation,
1145+
) -> None:
1146+
"""Test async_get_config_entry_implementation when one provider fails but another succeeds."""
1147+
1148+
async def failing_cloud_provider(
1149+
_hass: HomeAssistant, _domain: str
1150+
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
1151+
"""Provider that raises an exception."""
1152+
raise config_entry_oauth2_flow.ImplementationUnavailableError
1153+
1154+
async def successful_local_provider(
1155+
_hass: HomeAssistant, _domain: str
1156+
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
1157+
"""Provider that returns implementations."""
1158+
return [local_impl]
1159+
1160+
config_entry_oauth2_flow.async_add_implementation_provider(
1161+
hass, "cloud", failing_cloud_provider
1162+
)
1163+
config_entry_oauth2_flow.async_add_implementation_provider(
1164+
hass, "application_credentials", successful_local_provider
1165+
)
1166+
1167+
config_entry = MockConfigEntry(
1168+
domain=TEST_DOMAIN,
1169+
data={
1170+
"auth_implementation": local_impl.domain,
1171+
},
1172+
)
1173+
1174+
# This should succeed and return the local implementation
1175+
# even though the failing cloud provider raised an exception.
1176+
implementation = (
1177+
await config_entry_oauth2_flow.async_get_config_entry_implementation(
1178+
hass, config_entry
1179+
)
1180+
)
1181+
assert implementation is local_impl
1182+
1183+
1184+
async def test_async_get_config_entry_implementation_with_failing_provider(
1185+
hass: HomeAssistant,
1186+
) -> None:
1187+
"""Test async_get_config_entry_implementation when one provider fails and the other is empty."""
1188+
1189+
async def failing_cloud_provider(
1190+
_hass: HomeAssistant, _domain: str
1191+
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
1192+
"""Provider that raises an exception."""
1193+
raise config_entry_oauth2_flow.ImplementationUnavailableError
1194+
1195+
async def empty_local_provider(
1196+
_hass: HomeAssistant, _domain: str
1197+
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
1198+
"""Provider that returns implementations."""
1199+
return []
1200+
1201+
config_entry_oauth2_flow.async_add_implementation_provider(
1202+
hass, "cloud", failing_cloud_provider
1203+
)
1204+
config_entry_oauth2_flow.async_add_implementation_provider(
1205+
hass, "application_credentials", empty_local_provider
1206+
)
1207+
1208+
config_entry = MockConfigEntry(
1209+
domain=TEST_DOMAIN,
1210+
data={
1211+
"auth_implementation": TEST_DOMAIN,
1212+
},
1213+
)
1214+
1215+
# This should fail since the local provider returned an empty list
1216+
# and the cloud provider raised an exception.
1217+
with pytest.raises(config_entry_oauth2_flow.ImplementationUnavailableError):
1218+
await config_entry_oauth2_flow.async_get_config_entry_implementation(
1219+
hass, config_entry
1220+
)
1221+
1222+
1223+
async def test_async_get_config_entry_implementation_missing_provider(
1224+
hass: HomeAssistant,
1225+
) -> None:
1226+
"""Test async_get_config_entry_implementation when both providers are empty."""
1227+
1228+
async def empty_provider(
1229+
_hass: HomeAssistant, _domain: str
1230+
) -> list[config_entry_oauth2_flow.AbstractOAuth2Implementation]:
1231+
"""Provider that returns implementations."""
1232+
return []
1233+
1234+
config_entry_oauth2_flow.async_add_implementation_provider(
1235+
hass, "cloud", empty_provider
1236+
)
1237+
config_entry_oauth2_flow.async_add_implementation_provider(
1238+
hass, "application_credentials", empty_provider
1239+
)
1240+
1241+
config_entry = MockConfigEntry(
1242+
domain=TEST_DOMAIN,
1243+
data={
1244+
"auth_implementation": TEST_DOMAIN,
1245+
},
1246+
)
1247+
1248+
# This should fail since both providers are empty.
1249+
with pytest.raises(ValueError, match="Implementation not available"):
1250+
await config_entry_oauth2_flow.async_get_config_entry_implementation(
1251+
hass, config_entry
1252+
)

0 commit comments

Comments
 (0)