From f8318bbbc7dc3fecff6c46105675e04d95463832 Mon Sep 17 00:00:00 2001 From: Martin Hjelmare Date: Fri, 12 Jan 2024 09:47:08 +0100 Subject: [PATCH] Fix cloud tts loading (#107714) --- homeassistant/components/cloud/__init__.py | 20 ++++---- tests/components/cloud/conftest.py | 33 ++++++++---- tests/components/cloud/test_http_api.py | 4 +- tests/components/cloud/test_init.py | 20 +++++++- tests/components/cloud/test_system_health.py | 1 + tests/components/cloud/test_tts.py | 54 +++++++++++++++++++- 6 files changed, 108 insertions(+), 24 deletions(-) diff --git a/homeassistant/components/cloud/__init__.py b/homeassistant/components/cloud/__init__.py index 76369c07e8e..cdaae0d6272 100644 --- a/homeassistant/components/cloud/__init__.py +++ b/homeassistant/components/cloud/__init__.py @@ -294,7 +294,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: } async def _on_start() -> None: - """Discover platforms.""" + """Handle cloud started after login.""" nonlocal loaded # Prevent multiple discovery @@ -302,14 +302,6 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: return loaded = True - tts_info = {"platform_loaded": tts_platform_loaded} - - await async_load_platform(hass, Platform.TTS, DOMAIN, tts_info, config) - await tts_platform_loaded.wait() - - # The config entry should be loaded after the legacy tts platform is loaded - # to make sure that the tts integration is setup before we try to migrate - # old assist pipelines in the cloud stt entity. await hass.config_entries.flow.async_init(DOMAIN, context={"source": "system"}) async def _on_connect() -> None: @@ -338,6 +330,16 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: account_link.async_setup(hass) + hass.async_create_task( + async_load_platform( + hass, + Platform.TTS, + DOMAIN, + {"platform_loaded": tts_platform_loaded}, + config, + ) + ) + async_call_later( hass=hass, delay=timedelta(hours=STARTUP_REPAIR_DELAY), diff --git a/tests/components/cloud/conftest.py b/tests/components/cloud/conftest.py index 42852b15206..1e1877ae13c 100644 --- a/tests/components/cloud/conftest.py +++ b/tests/components/cloud/conftest.py @@ -76,16 +76,9 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: # Attributes that we mock with default values. - mock_cloud.id_token = jwt.encode( - { - "email": "hello@home-assistant.io", - "custom:sub-exp": "2018-01-03", - "cognito:username": "abcdefghjkl", - }, - "test", - ) - mock_cloud.access_token = "test_access_token" - mock_cloud.refresh_token = "test_refresh_token" + mock_cloud.id_token = None + mock_cloud.access_token = None + mock_cloud.refresh_token = None # Properties that we keep as properties. @@ -122,11 +115,31 @@ async def cloud_fixture() -> AsyncGenerator[MagicMock, None]: When called, it should call the on_start callback. """ + mock_cloud.id_token = jwt.encode( + { + "email": "hello@home-assistant.io", + "custom:sub-exp": "2018-01-03", + "cognito:username": "abcdefghjkl", + }, + "test", + ) + mock_cloud.access_token = "test_access_token" + mock_cloud.refresh_token = "test_refresh_token" on_start_callback = mock_cloud.register_on_start.call_args[0][0] await on_start_callback() mock_cloud.login.side_effect = mock_login + async def mock_logout() -> None: + """Mock logout.""" + mock_cloud.id_token = None + mock_cloud.access_token = None + mock_cloud.refresh_token = None + await mock_cloud.stop() + await mock_cloud.client.logout_cleanups() + + mock_cloud.logout.side_effect = mock_logout + yield mock_cloud diff --git a/tests/components/cloud/test_http_api.py b/tests/components/cloud/test_http_api.py index 29930632691..409d86d6e37 100644 --- a/tests/components/cloud/test_http_api.py +++ b/tests/components/cloud/test_http_api.py @@ -113,8 +113,8 @@ async def setup_cloud_fixture(hass: HomeAssistant, cloud: MagicMock) -> None: }, ) await hass.async_block_till_done() - on_start_callback = cloud.register_on_start.call_args[0][0] - await on_start_callback() + await cloud.login("test-user", "test-pass") + cloud.login.reset_mock() async def test_google_actions_sync( diff --git a/tests/components/cloud/test_init.py b/tests/components/cloud/test_init.py index 850f8e12e02..c537169bf01 100644 --- a/tests/components/cloud/test_init.py +++ b/tests/components/cloud/test_init.py @@ -19,7 +19,7 @@ from homeassistant.core import Context, HomeAssistant from homeassistant.exceptions import Unauthorized from homeassistant.setup import async_setup_component -from tests.common import MockUser +from tests.common import MockConfigEntry, MockUser async def test_constructor_loads_info_from_config(hass: HomeAssistant) -> None: @@ -230,6 +230,7 @@ async def test_async_get_or_create_cloudhook( """Test async_get_or_create_cloudhook.""" assert await async_setup_component(hass, "cloud", {"cloud": {}}) await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") webhook_id = "mock-webhook-id" cloudhook_url = "https://cloudhook.nabu.casa/abcdefg" @@ -262,7 +263,7 @@ async def test_async_get_or_create_cloudhook( async_create_cloudhook_mock.assert_not_called() # Simulate logged out - cloud.id_token = None + await cloud.logout() # Not logged in with pytest.raises(CloudNotAvailable): @@ -274,3 +275,18 @@ async def test_async_get_or_create_cloudhook( # Not connected with pytest.raises(CloudNotConnected): await async_get_or_create_cloudhook(hass, webhook_id) + + +async def test_cloud_logout( + hass: HomeAssistant, + cloud: MagicMock, +) -> None: + """Test cloud setup with existing config entry when user is logged out.""" + assert cloud.is_logged_in is False + + mock_config_entry = MockConfigEntry(domain=DOMAIN) + mock_config_entry.add_to_hass(hass) + assert await async_setup_component(hass, DOMAIN, {"cloud": {}}) + await hass.async_block_till_done() + + assert cloud.is_logged_in is False diff --git a/tests/components/cloud/test_system_health.py b/tests/components/cloud/test_system_health.py index 9f1af8aaeb4..5480cd557fd 100644 --- a/tests/components/cloud/test_system_health.py +++ b/tests/components/cloud/test_system_health.py @@ -42,6 +42,7 @@ async def test_cloud_system_health( }, ) await hass.async_block_till_done() + await cloud.login("test-user", "test-pass") cloud.remote.snitun_server = "us-west-1" cloud.remote.certificate_status = CertificateStatus.READY diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index dc32747182d..4069edcb744 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -4,7 +4,7 @@ from http import HTTPStatus from typing import Any from unittest.mock import AsyncMock, MagicMock -from hass_nabucasa.voice import MAP_VOICE, VoiceError +from hass_nabucasa.voice import MAP_VOICE, VoiceError, VoiceTokenError import pytest import voluptuous as vol @@ -189,3 +189,55 @@ async def test_get_tts_audio( assert mock_process_tts.call_args.kwargs["language"] == "en-US" assert mock_process_tts.call_args.kwargs["gender"] == "female" assert mock_process_tts.call_args.kwargs["output"] == "mp3" + + +@pytest.mark.parametrize( + ("data", "expected_url_suffix"), + [ + ({"platform": DOMAIN}, DOMAIN), + ({"engine_id": DOMAIN}, DOMAIN), + ], +) +async def test_get_tts_audio_logged_out( + hass: HomeAssistant, + hass_client: ClientSessionGenerator, + cloud: MagicMock, + data: dict[str, Any], + expected_url_suffix: str, +) -> None: + """Test cloud get tts audio when user is logged out.""" + mock_process_tts = AsyncMock( + side_effect=VoiceTokenError("No token!"), + ) + cloud.voice.process_tts = mock_process_tts + assert await async_setup_component(hass, "homeassistant", {}) + assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}}) + await hass.async_block_till_done() + client = await hass_client() + + url = "/api/tts_get_url" + data |= {"message": "There is someone at the door."} + + req = await client.post(url, json=data) + assert req.status == HTTPStatus.OK + response = await req.json() + + assert response == { + "url": ( + "http://example.local:8123/api/tts_proxy/" + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + ), + "path": ( + "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_en-us_e09b5a0968_{expected_url_suffix}.mp3" + ), + } + await hass.async_block_till_done() + + assert mock_process_tts.call_count == 1 + assert mock_process_tts.call_args is not None + assert mock_process_tts.call_args.kwargs["text"] == "There is someone at the door." + assert mock_process_tts.call_args.kwargs["language"] == "en-US" + assert mock_process_tts.call_args.kwargs["gender"] == "female" + assert mock_process_tts.call_args.kwargs["output"] == "mp3"