mirror of
https://github.com/home-assistant/core.git
synced 2025-09-05 12:51:37 +02:00
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:
@@ -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:
|
||||
|
@@ -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
|
||||
|
Reference in New Issue
Block a user