mirror of
https://github.com/home-assistant/core.git
synced 2025-09-05 21:01: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 collections.abc import Callable
|
||||||
from contextlib import suppress
|
from contextlib import suppress
|
||||||
from datetime import datetime, timedelta
|
from datetime import datetime, timedelta
|
||||||
from http import HTTPStatus
|
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any
|
from typing import TYPE_CHECKING, Any
|
||||||
|
|
||||||
import aiohttp
|
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 yarl import URL
|
||||||
|
|
||||||
from homeassistant.components import persistent_notification
|
from homeassistant.components import persistent_notification
|
||||||
@@ -146,7 +150,7 @@ class CloudAlexaConfig(alexa_config.AbstractConfig):
|
|||||||
self._cloud_user = cloud_user
|
self._cloud_user = cloud_user
|
||||||
self._prefs = prefs
|
self._prefs = prefs
|
||||||
self._cloud = cloud
|
self._cloud = cloud
|
||||||
self._token = None
|
self._token: str | None = None
|
||||||
self._token_valid: datetime | None = None
|
self._token_valid: datetime | None = None
|
||||||
self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA)
|
self._cur_entity_prefs = async_get_assistant_settings(hass, CLOUD_ALEXA)
|
||||||
self._alexa_sync_unsub: Callable[[], None] | None = None
|
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:
|
async def async_get_access_token(self) -> str | None:
|
||||||
"""Get an access token."""
|
"""Get an access token."""
|
||||||
|
details: AlexaAccessTokenDetails | None
|
||||||
if self._token_valid is not None and self._token_valid > utcnow():
|
if self._token_valid is not None and self._token_valid > utcnow():
|
||||||
return self._token
|
return self._token
|
||||||
|
|
||||||
resp = await cloud_api.async_alexa_access_token(self._cloud)
|
try:
|
||||||
body = await resp.json()
|
details = await self._cloud.alexa_api.access_token()
|
||||||
|
except AlexaApiNeedsRelinkError as exception:
|
||||||
if resp.status == HTTPStatus.BAD_REQUEST:
|
|
||||||
if body["reason"] in ("RefreshTokenNotFound", "UnknownRegion"):
|
|
||||||
if self.should_report_state:
|
if self.should_report_state:
|
||||||
persistent_notification.async_create(
|
persistent_notification.async_create(
|
||||||
self.hass,
|
self.hass,
|
||||||
(
|
(
|
||||||
"There was an error reporting state to Alexa"
|
"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."
|
" the Alexa app to continue using it."
|
||||||
),
|
),
|
||||||
"Alexa state reporting disabled",
|
"Alexa state reporting disabled",
|
||||||
"cloud_alexa_report",
|
"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 = details["access_token"]
|
||||||
|
self._endpoint = details["event_endpoint"]
|
||||||
self._token = body["access_token"]
|
self._token_valid = utcnow() + timedelta(seconds=details["expires_in"])
|
||||||
self._endpoint = body["event_endpoint"]
|
|
||||||
self._token_valid = utcnow() + timedelta(seconds=body["expires_in"])
|
|
||||||
return self._token
|
return self._token
|
||||||
|
|
||||||
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
|
async def _async_prefs_updated(self, prefs: CloudPreferences) -> None:
|
||||||
|
@@ -3,6 +3,11 @@
|
|||||||
import contextlib
|
import contextlib
|
||||||
from unittest.mock import AsyncMock, Mock, patch
|
from unittest.mock import AsyncMock, Mock, patch
|
||||||
|
|
||||||
|
from hass_nabucasa.alexa_api import (
|
||||||
|
AlexaApiError,
|
||||||
|
AlexaApiNeedsRelinkError,
|
||||||
|
AlexaApiNoTokenError,
|
||||||
|
)
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from homeassistant.components.alexa import errors
|
from homeassistant.components.alexa import errors
|
||||||
@@ -195,30 +200,40 @@ async def test_alexa_config_invalidate_token(
|
|||||||
servicehandlers_server="example",
|
servicehandlers_server="example",
|
||||||
auth=Mock(async_check_token=AsyncMock()),
|
auth=Mock(async_check_token=AsyncMock()),
|
||||||
websession=async_get_clientsession(hass),
|
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()
|
token = await conf.async_get_access_token()
|
||||||
assert token == "mock-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()
|
token = await conf.async_get_access_token()
|
||||||
assert token == "mock-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
|
assert conf._token_valid is not None
|
||||||
conf.async_invalidate_access_token()
|
conf.async_invalidate_access_token()
|
||||||
assert conf._token_valid is None
|
assert conf._token_valid is None
|
||||||
token = await conf.async_get_access_token()
|
token = await conf.async_get_access_token()
|
||||||
assert token == "mock-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(
|
@pytest.mark.parametrize(
|
||||||
("reject_reason", "expected_exception"),
|
("lib_exception", "expected_exception"),
|
||||||
[
|
[
|
||||||
("RefreshTokenNotFound", errors.RequireRelink),
|
(AlexaApiNeedsRelinkError("Needs relink"), errors.RequireRelink),
|
||||||
("UnknownRegion", errors.RequireRelink),
|
(AlexaApiNeedsRelinkError("UnknownRegion"), errors.RequireRelink),
|
||||||
("OtherReason", errors.NoTokenAvailable),
|
(AlexaApiNoTokenError("OtherReason"), errors.NoTokenAvailable),
|
||||||
|
(AlexaApiError("OtherReason"), errors.NoTokenAvailable),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
async def test_alexa_config_fail_refresh_token(
|
async def test_alexa_config_fail_refresh_token(
|
||||||
@@ -226,7 +241,7 @@ async def test_alexa_config_fail_refresh_token(
|
|||||||
cloud_prefs: CloudPreferences,
|
cloud_prefs: CloudPreferences,
|
||||||
aioclient_mock: AiohttpClientMocker,
|
aioclient_mock: AiohttpClientMocker,
|
||||||
entity_registry: er.EntityRegistry,
|
entity_registry: er.EntityRegistry,
|
||||||
reject_reason: str,
|
lib_exception: Exception,
|
||||||
expected_exception: type[Exception],
|
expected_exception: type[Exception],
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Test Alexa config failing to refresh token."""
|
"""Test Alexa config failing to refresh token."""
|
||||||
@@ -259,6 +274,15 @@ async def test_alexa_config_fail_refresh_token(
|
|||||||
servicehandlers_server="example",
|
servicehandlers_server="example",
|
||||||
auth=Mock(async_check_token=AsyncMock()),
|
auth=Mock(async_check_token=AsyncMock()),
|
||||||
websession=async_get_clientsession(hass),
|
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()
|
await conf.async_initialize()
|
||||||
@@ -284,12 +308,7 @@ async def test_alexa_config_fail_refresh_token(
|
|||||||
|
|
||||||
# Invalidate the token and try to fetch another
|
# Invalidate the token and try to fetch another
|
||||||
conf.async_invalidate_access_token()
|
conf.async_invalidate_access_token()
|
||||||
aioclient_mock.clear_requests()
|
conf._cloud.alexa_api.access_token.side_effect = lib_exception
|
||||||
aioclient_mock.post(
|
|
||||||
"https://example/alexa/access_token",
|
|
||||||
json={"reason": reject_reason},
|
|
||||||
status=400,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Change states to trigger event listener
|
# Change states to trigger event listener
|
||||||
hass.states.async_set(entity_entry.entity_id, "off")
|
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
|
# Simulate we're again authorized and token update succeeds
|
||||||
# State reporting should now be re-enabled for Alexa
|
# State reporting should now be re-enabled for Alexa
|
||||||
aioclient_mock.clear_requests()
|
conf._cloud.alexa_api.access_token.side_effect = None
|
||||||
aioclient_mock.post(
|
|
||||||
"https://example/alexa/access_token",
|
|
||||||
json={
|
|
||||||
"access_token": "mock-token",
|
|
||||||
"event_endpoint": "http://example.com/alexa_endpoint",
|
|
||||||
"expires_in": 30,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
await conf.set_authorized(True)
|
await conf.set_authorized(True)
|
||||||
assert cloud_prefs.alexa_report_state is True
|
assert cloud_prefs.alexa_report_state is True
|
||||||
assert conf.should_report_state is True
|
assert conf.should_report_state is True
|
||||||
|
Reference in New Issue
Block a user