Move alexa access token updates to new handler (#150466)

Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Joakim Sørensen
2025-08-13 13:21:28 +02:00
committed by GitHub
parent 5ad2a27918
commit eea04558a9
2 changed files with 63 additions and 48 deletions

View File

@@ -6,12 +6,16 @@ import asyncio
from collections.abc import Callable
from contextlib import suppress
from datetime import datetime, timedelta
from http import HTTPStatus
import logging
from typing import TYPE_CHECKING, Any
import aiohttp
from hass_nabucasa import Cloud, cloud_api
from hass_nabucasa import AlexaApiError, Cloud
from hass_nabucasa.alexa_api import (
AlexaAccessTokenDetails,
AlexaApiNeedsRelinkError,
AlexaApiNoTokenError,
)
from yarl import URL
from homeassistant.components import persistent_notification
@@ -146,7 +150,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
self._cloud_user = cloud_user
self._prefs = prefs
self._cloud = cloud
self._token = None
self._token: str | None = None
self._token_valid: datetime | None = None
self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA)
self._alexa_sync_unsub: Callable[[], None] | None = None
@@ -318,32 +322,31 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
async def async_get_access_token(self) -> str | None:
"""Get an access token."""
details: AlexaAccessTokenDetails | None
if self._token_valid is not None and self._token_valid > utcnow():
return self._token
resp = await cloud_api.async_alexa_access_token(self._cloud)
body = await resp.json()
if resp.status == HTTPStatus.BAD_REQUEST:
if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"):
try:
details = await self._cloud.alexa_api.access_token()
except AlexaApiNeedsRelinkError as exception:
if self.should_report_state:
persistent_notification.async_create(
self.hass,
(
"There was an error reporting state to Alexa"
f" ({body['reason']}). Please re-link your Alexa skill via"
f" ({exception.reason}). Please re-link your Alexa skill via"
" the Alexa app to continue using it."
),
"Alexa state reporting disabled",
"cloud_alexa_report",
)
raise alexa_errors.RequireRelink
raise alexa_errors.RequireRelink from exception
except (AlexaApiNoTokenError, AlexaApiError) as exception:
raise alexa_errors.NoTokenAvailable from exception
raise alexa_errors.NoTokenAvailable
self._token = body["access_token"]
self._endpoint = body["event_endpoint"]
self._token_valid = utcnow() + timedelta(seconds=body["expires_in"])
self._token = details["access_token"]
self._endpoint = details["event_endpoint"]
self._token_valid = utcnow() + timedelta(seconds=details["expires_in"])
return self._token
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:

View File

@@ -3,6 +3,11 @@
import contextlib
from unittest.mock import AsyncMock, Mock, patch
from hass_nabucasa.alexa_api import (
AlexaApiError,
AlexaApiNeedsRelinkError,
AlexaApiNoTokenError,
)
import pytest
from homeassistant.components.alexa import errors
@@ -195,30 +200,40 @@ async def test_alexa_config_invalidate_token(
servicehandlers_server="example",
auth=Mock(async_check_token=AsyncMock()),
websession=async_get_clientsession(hass),
alexa_api=Mock(
access_token=AsyncMock(
return_value={
"access_token": "mock-token",
"event_endpoint": "http://example.com/alexa_endpoint",
"expires_in": 30,
}
)
),
),
)
token = await conf.async_get_access_token()
assert token == "mock-token"
assert len(aioclient_mock.mock_calls) == 1
assert len(conf._cloud.alexa_api.access_token.mock_calls) == 1
token = await conf.async_get_access_token()
assert token == "mock-token"
assert len(aioclient_mock.mock_calls) == 1
assert len(conf._cloud.alexa_api.access_token.mock_calls) == 1
assert conf._token_valid is not None
conf.async_invalidate_access_token()
assert conf._token_valid is None
token = await conf.async_get_access_token()
assert token == "mock-token"
assert len(aioclient_mock.mock_calls) == 2
assert len(conf._cloud.alexa_api.access_token.mock_calls) == 2
@pytest.mark.parametrize(
("reject_reason", "expected_exception"),
("lib_exception", "expected_exception"),
[
("RefreshTokenNotFound", errors.RequireRelink),
("UnknownRegion", errors.RequireRelink),
("OtherReason", errors.NoTokenAvailable),
(AlexaApiNeedsRelinkError("Needs relink"), errors.RequireRelink),
(AlexaApiNeedsRelinkError("UnknownRegion"), errors.RequireRelink),
(AlexaApiNoTokenError("OtherReason"), errors.NoTokenAvailable),
(AlexaApiError("OtherReason"), errors.NoTokenAvailable),
],
)
async def test_alexa_config_fail_refresh_token(
@@ -226,7 +241,7 @@ async def test_alexa_config_fail_refresh_token(
cloud_prefs: CloudPreferences,
aioclient_mock: AiohttpClientMocker,
entity_registry: er.EntityRegistry,
reject_reason: str,
lib_exception: Exception,
expected_exception: type[Exception],
) -> None:
"""Test Alexa config failing to refresh token."""
@@ -259,6 +274,15 @@ async def test_alexa_config_fail_refresh_token(
servicehandlers_server="example",
auth=Mock(async_check_token=AsyncMock()),
websession=async_get_clientsession(hass),
alexa_api=Mock(
access_token=AsyncMock(
return_value={
"access_token": "mock-token",
"event_endpoint": "http://example.com/alexa_endpoint",
"expires_in": 30,
}
)
),
),
)
await conf.async_initialize()
@@ -284,12 +308,7 @@ async def test_alexa_config_fail_refresh_token(
# Invalidate the token and try to fetch another
conf.async_invalidate_access_token()
aioclient_mock.clear_requests()
aioclient_mock.post(
"https://example/alexa/access_token",
json={"reason": reject_reason},
status=400,
)
conf._cloud.alexa_api.access_token.side_effect = lib_exception
# Change states to trigger event listener
hass.states.async_set(entity_entry.entity_id, "off")
@@ -310,15 +329,8 @@ async def test_alexa_config_fail_refresh_token(
# Simulate we're again authorized and token update succeeds
# State reporting should now be re-enabled for Alexa
aioclient_mock.clear_requests()
aioclient_mock.post(
"https://example/alexa/access_token",
json={
"access_token": "mock-token",
"event_endpoint": "http://example.com/alexa_endpoint",
"expires_in": 30,
},
)
conf._cloud.alexa_api.access_token.side_effect = None
await conf.set_authorized(True)
assert cloud_prefs.alexa_report_state is True
assert conf.should_report_state is True