From d4071e7123d05acbda07864506541f879fb563cd Mon Sep 17 00:00:00 2001 From: Michael Hansen Date: Sun, 24 Nov 2024 19:52:21 -0600 Subject: [PATCH] Hide TTS filename behind random token (#131192) * Hide TTS filename behind random token * Clean up and fix test snapshots * Fix tests * Fix cloud tests --- homeassistant/components/tts/__init__.py | 24 +- .../assist_pipeline/snapshots/test_init.ambr | 8 +- .../snapshots/test_websocket.ambr | 8 +- tests/components/assist_pipeline/test_init.py | 148 ++-- .../assist_pipeline/test_websocket.py | 683 +++++++++--------- tests/components/cloud/test_tts.py | 240 +++--- tests/components/tts/test_init.py | 254 ++++--- 7 files changed, 701 insertions(+), 664 deletions(-) diff --git a/homeassistant/components/tts/__init__.py b/homeassistant/components/tts/__init__.py index ad267b9106b9d6..e7d1091719bf5d 100644 --- a/homeassistant/components/tts/__init__.py +++ b/homeassistant/components/tts/__init__.py @@ -13,6 +13,7 @@ import mimetypes import os import re +import secrets import subprocess import tempfile from typing import Any, Final, TypedDict, final @@ -540,6 +541,10 @@ def __init__( self.file_cache: dict[str, str] = {} self.mem_cache: dict[str, TTSCache] = {} + # filename <-> token + self.filename_to_token: dict[str, str] = {} + self.token_to_filename: dict[str, str] = {} + def _init_cache(self) -> dict[str, str]: """Init cache folder and fetch files.""" try: @@ -656,7 +661,17 @@ async def async_get_url_path( engine_instance, cache_key, message, use_cache, language, options ) - return f"/api/tts_proxy/{filename}" + # Use a randomly generated token instead of exposing the filename + token = self.filename_to_token.get(filename) + if not token: + # Keep extension (.mp3, etc.) + token = secrets.token_urlsafe(16) + os.path.splitext(filename)[1] + + # Map token <-> filename + self.filename_to_token[filename] = token + self.token_to_filename[token] = filename + + return f"/api/tts_proxy/{token}" async def async_get_tts_audio( self, @@ -910,11 +925,15 @@ def async_remove_from_mem(_: datetime) -> None: ), ) - async def async_read_tts(self, filename: str) -> tuple[str | None, bytes]: + async def async_read_tts(self, token: str) -> tuple[str | None, bytes]: """Read a voice file and return binary. This method is a coroutine. """ + filename = self.token_to_filename.get(token) + if not filename: + raise HomeAssistantError(f"{token} was not recognized!") + if not (record := _RE_VOICE_FILE.match(filename.lower())) and not ( record := _RE_LEGACY_VOICE_FILE.match(filename.lower()) ): @@ -1076,6 +1095,7 @@ def __init__(self, tts: SpeechManager) -> None: async def get(self, request: web.Request, filename: str) -> web.Response: """Start a get request.""" try: + # filename is actually token, but we keep its name for compatibility content, data = await self.tts.async_read_tts(filename) except HomeAssistantError as err: _LOGGER.error("Error on load tts: %s", err) diff --git a/tests/components/assist_pipeline/snapshots/test_init.ambr b/tests/components/assist_pipeline/snapshots/test_init.ambr index 7f77dada3be989..c70d3944f88b6f 100644 --- a/tests/components/assist_pipeline/snapshots/test_init.ambr +++ b/tests/components/assist_pipeline/snapshots/test_init.ambr @@ -77,7 +77,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }), 'type': , @@ -166,7 +166,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22Arnold+Schwarzenegger%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_2657c1a8ee_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }), 'type': , @@ -255,7 +255,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22Arnold+Schwarzenegger%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_2657c1a8ee_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }), 'type': , @@ -368,7 +368,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }), 'type': , diff --git a/tests/components/assist_pipeline/snapshots/test_websocket.ambr b/tests/components/assist_pipeline/snapshots/test_websocket.ambr index b806c6faf23eb5..566fb129959d2f 100644 --- a/tests/components/assist_pipeline/snapshots/test_websocket.ambr +++ b/tests/components/assist_pipeline/snapshots/test_websocket.ambr @@ -73,7 +73,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }) # --- @@ -154,7 +154,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }) # --- @@ -247,7 +247,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }) # --- @@ -350,7 +350,7 @@ 'tts_output': dict({ 'media_id': "media-source://tts/test?message=Sorry,+I+couldn't+understand+that&language=en-US&tts_options=%7B%22voice%22:%22james_earl_jones%22%7D", 'mime_type': 'audio/mpeg', - 'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', + 'url': '/api/tts_proxy/test_token.mp3', }), }) # --- diff --git a/tests/components/assist_pipeline/test_init.py b/tests/components/assist_pipeline/test_init.py index a8ada11fb45310..b177530219e41d 100644 --- a/tests/components/assist_pipeline/test_init.py +++ b/tests/components/assist_pipeline/test_init.py @@ -70,21 +70,24 @@ async def audio_data(): yield make_10ms_chunk(b"part2") yield b"" - await assist_pipeline.async_pipeline_from_audio_stream( - hass, - context=Context(), - event_callback=events.append, - stt_metadata=stt.SpeechMetadata( - language="", - format=stt.AudioFormats.WAV, - codec=stt.AudioCodecs.PCM, - bit_rate=stt.AudioBitRates.BITRATE_16, - sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, - channel=stt.AudioChannels.CHANNEL_MONO, - ), - stt_stream=audio_data(), - audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( + language="", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + stt_stream=audio_data(), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), + ) assert process_events(events) == snapshot assert len(mock_stt_provider_entity.received) == 2 @@ -133,23 +136,26 @@ async def audio_data(): assert msg["success"] pipeline_id = msg["result"]["id"] - # Use the created pipeline - await assist_pipeline.async_pipeline_from_audio_stream( - hass, - context=Context(), - event_callback=events.append, - stt_metadata=stt.SpeechMetadata( - language="en-UK", - format=stt.AudioFormats.WAV, - codec=stt.AudioCodecs.PCM, - bit_rate=stt.AudioBitRates.BITRATE_16, - sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, - channel=stt.AudioChannels.CHANNEL_MONO, - ), - stt_stream=audio_data(), - pipeline_id=pipeline_id, - audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + # Use the created pipeline + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( + language="en-UK", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + stt_stream=audio_data(), + pipeline_id=pipeline_id, + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), + ) assert process_events(events) == snapshot assert len(mock_stt_provider.received) == 2 @@ -198,23 +204,26 @@ async def audio_data(): assert msg["success"] pipeline_id = msg["result"]["id"] - # Use the created pipeline - await assist_pipeline.async_pipeline_from_audio_stream( - hass, - context=Context(), - event_callback=events.append, - stt_metadata=stt.SpeechMetadata( - language="en-UK", - format=stt.AudioFormats.WAV, - codec=stt.AudioCodecs.PCM, - bit_rate=stt.AudioBitRates.BITRATE_16, - sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, - channel=stt.AudioChannels.CHANNEL_MONO, - ), - stt_stream=audio_data(), - pipeline_id=pipeline_id, - audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + # Use the created pipeline + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( + language="en-UK", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + stt_stream=audio_data(), + pipeline_id=pipeline_id, + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), + ) assert process_events(events) == snapshot assert len(mock_stt_provider_entity.received) == 2 @@ -362,25 +371,28 @@ async def audio_data(): yield b"" - await assist_pipeline.async_pipeline_from_audio_stream( - hass, - context=Context(), - event_callback=events.append, - stt_metadata=stt.SpeechMetadata( - language="", - format=stt.AudioFormats.WAV, - codec=stt.AudioCodecs.PCM, - bit_rate=stt.AudioBitRates.BITRATE_16, - sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, - channel=stt.AudioChannels.CHANNEL_MONO, - ), - stt_stream=audio_data(), - start_stage=assist_pipeline.PipelineStage.WAKE_WORD, - wake_word_settings=assist_pipeline.WakeWordSettings( - audio_seconds_to_buffer=1.5 - ), - audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await assist_pipeline.async_pipeline_from_audio_stream( + hass, + context=Context(), + event_callback=events.append, + stt_metadata=stt.SpeechMetadata( + language="", + format=stt.AudioFormats.WAV, + codec=stt.AudioCodecs.PCM, + bit_rate=stt.AudioBitRates.BITRATE_16, + sample_rate=stt.AudioSampleRates.SAMPLERATE_16000, + channel=stt.AudioChannels.CHANNEL_MONO, + ), + stt_stream=audio_data(), + start_stage=assist_pipeline.PipelineStage.WAKE_WORD, + wake_word_settings=assist_pipeline.WakeWordSettings( + audio_seconds_to_buffer=1.5 + ), + audio_settings=assist_pipeline.AudioSettings(is_vad_enabled=False), + ) assert process_events(events) == snapshot diff --git a/tests/components/assist_pipeline/test_websocket.py b/tests/components/assist_pipeline/test_websocket.py index c9bc3ef41de365..c1caf6f86a49f5 100644 --- a/tests/components/assist_pipeline/test_websocket.py +++ b/tests/components/assist_pipeline/test_websocket.py @@ -119,85 +119,88 @@ async def test_audio_pipeline( events = [] client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "assist_pipeline/run", - "start_stage": "stt", - "end_stage": "tts", - "input": { - "sample_rate": 44100, - }, - } - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "stt", + "end_stage": "tts", + "input": { + "sample_rate": 44100, + }, + } + ) - # result - msg = await client.receive_json() - assert msg["success"] + # result + msg = await client.receive_json() + assert msg["success"] - # run start - msg = await client.receive_json() - assert msg["event"]["type"] == "run-start" - msg["event"]["data"]["pipeline"] = ANY - assert msg["event"]["data"] == snapshot - handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] - events.append(msg["event"]) + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] + events.append(msg["event"]) - # stt - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # stt + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # End of audio stream (handler id + empty payload) - await client.send_bytes(bytes([handler_id])) + # End of audio stream (handler id + empty payload) + await client.send_bytes(bytes([handler_id])) - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # intent - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # intent + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # text-to-speech - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # text-to-speech + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # run end - msg = await client.receive_json() - assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - pipeline_data: PipelineData = hass.data[DOMAIN] - pipeline_id = list(pipeline_data.pipeline_debug)[0] - pipeline_run_id = list(pipeline_data.pipeline_debug[pipeline_id])[0] + pipeline_data: PipelineData = hass.data[DOMAIN] + pipeline_id = list(pipeline_data.pipeline_debug)[0] + pipeline_run_id = list(pipeline_data.pipeline_debug[pipeline_id])[0] - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline_debug/get", - "pipeline_id": pipeline_id, - "pipeline_run_id": pipeline_run_id, - } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == {"events": events} + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline_debug/get", + "pipeline_id": pipeline_id, + "pipeline_run_id": pipeline_run_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"events": events} async def test_audio_pipeline_with_wake_word_timeout( @@ -210,49 +213,52 @@ async def test_audio_pipeline_with_wake_word_timeout( events = [] client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "assist_pipeline/run", - "start_stage": "wake_word", - "end_stage": "tts", - "input": { - "sample_rate": SAMPLE_RATE, - "timeout": 1, - }, - } - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": { + "sample_rate": SAMPLE_RATE, + "timeout": 1, + }, + } + ) - # result - msg = await client.receive_json() - assert msg["success"], msg + # result + msg = await client.receive_json() + assert msg["success"], msg - # run start - msg = await client.receive_json() - assert msg["event"]["type"] == "run-start" - msg["event"]["data"]["pipeline"] = ANY - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # wake_word - msg = await client.receive_json() - assert msg["event"]["type"] == "wake_word-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # wake_word + msg = await client.receive_json() + assert msg["event"]["type"] == "wake_word-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # 2 seconds of silence - await client.send_bytes(bytes([1]) + bytes(2 * BYTES_ONE_SECOND)) + # 2 seconds of silence + await client.send_bytes(bytes([1]) + bytes(2 * BYTES_ONE_SECOND)) - # Time out error - msg = await client.receive_json() - assert msg["event"]["type"] == "error" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # Time out error + msg = await client.receive_json() + assert msg["event"]["type"] == "error" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # run end - msg = await client.receive_json() - assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) async def test_audio_pipeline_with_wake_word_no_timeout( @@ -265,98 +271,101 @@ async def test_audio_pipeline_with_wake_word_no_timeout( events = [] client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "assist_pipeline/run", - "start_stage": "wake_word", - "end_stage": "tts", - "input": {"sample_rate": SAMPLE_RATE, "timeout": 0, "no_vad": True}, - } - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "wake_word", + "end_stage": "tts", + "input": {"sample_rate": SAMPLE_RATE, "timeout": 0, "no_vad": True}, + } + ) - # result - msg = await client.receive_json() - assert msg["success"], msg + # result + msg = await client.receive_json() + assert msg["success"], msg - # run start - msg = await client.receive_json() - assert msg["event"]["type"] == "run-start" - msg["event"]["data"]["pipeline"] = ANY - assert msg["event"]["data"] == snapshot - handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] - events.append(msg["event"]) + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] + events.append(msg["event"]) - # wake_word - msg = await client.receive_json() - assert msg["event"]["type"] == "wake_word-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # wake_word + msg = await client.receive_json() + assert msg["event"]["type"] == "wake_word-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # "audio" - await client.send_bytes(bytes([handler_id]) + make_10ms_chunk(b"wake word")) + # "audio" + await client.send_bytes(bytes([handler_id]) + make_10ms_chunk(b"wake word")) - async with asyncio.timeout(1): - msg = await client.receive_json() - assert msg["event"]["type"] == "wake_word-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + async with asyncio.timeout(1): + msg = await client.receive_json() + assert msg["event"]["type"] == "wake_word-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # stt - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # stt + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # End of audio stream (handler id + empty payload) - await client.send_bytes(bytes([handler_id])) + # End of audio stream (handler id + empty payload) + await client.send_bytes(bytes([handler_id])) - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # intent - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # intent + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # text-to-speech - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # text-to-speech + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # run end - msg = await client.receive_json() - assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - pipeline_data: PipelineData = hass.data[DOMAIN] - pipeline_id = list(pipeline_data.pipeline_debug)[0] - pipeline_run_id = list(pipeline_data.pipeline_debug[pipeline_id])[0] + pipeline_data: PipelineData = hass.data[DOMAIN] + pipeline_id = list(pipeline_data.pipeline_debug)[0] + pipeline_run_id = list(pipeline_data.pipeline_debug[pipeline_id])[0] - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline_debug/get", - "pipeline_id": pipeline_id, - "pipeline_run_id": pipeline_run_id, - } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == {"events": events} + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline_debug/get", + "pipeline_id": pipeline_id, + "pipeline_run_id": pipeline_run_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"events": events} async def test_audio_pipeline_no_wake_word_engine( @@ -1540,99 +1549,102 @@ async def test_audio_pipeline_debug( events = [] client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "assist_pipeline/run", - "start_stage": "stt", - "end_stage": "tts", - "input": { - "sample_rate": 44100, - }, - } - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "stt", + "end_stage": "tts", + "input": { + "sample_rate": 44100, + }, + } + ) - # result - msg = await client.receive_json() - assert msg["success"] + # result + msg = await client.receive_json() + assert msg["success"] - # run start - msg = await client.receive_json() - assert msg["event"]["type"] == "run-start" - msg["event"]["data"]["pipeline"] = ANY - assert msg["event"]["data"] == snapshot - handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] - events.append(msg["event"]) + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] + events.append(msg["event"]) - # stt - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # stt + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # End of audio stream (handler id + empty payload) - await client.send_bytes(bytes([handler_id])) + # End of audio stream (handler id + empty payload) + await client.send_bytes(bytes([handler_id])) - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # intent - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # intent + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # text-to-speech - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # text-to-speech + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # run end - msg = await client.receive_json() - assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # Get the id of the pipeline - await client.send_json_auto_id({"type": "assist_pipeline/pipeline/list"}) - msg = await client.receive_json() - assert msg["success"] - assert len(msg["result"]["pipelines"]) == 1 + # Get the id of the pipeline + await client.send_json_auto_id({"type": "assist_pipeline/pipeline/list"}) + msg = await client.receive_json() + assert msg["success"] + assert len(msg["result"]["pipelines"]) == 1 - pipeline_id = msg["result"]["pipelines"][0]["id"] + pipeline_id = msg["result"]["pipelines"][0]["id"] - # Get the id for the run - await client.send_json_auto_id( - {"type": "assist_pipeline/pipeline_debug/list", "pipeline_id": pipeline_id} - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == {"pipeline_runs": [ANY]} + # Get the id for the run + await client.send_json_auto_id( + {"type": "assist_pipeline/pipeline_debug/list", "pipeline_id": pipeline_id} + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"pipeline_runs": [ANY]} - pipeline_run_id = msg["result"]["pipeline_runs"][0]["pipeline_run_id"] + pipeline_run_id = msg["result"]["pipeline_runs"][0]["pipeline_run_id"] - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline_debug/get", - "pipeline_id": pipeline_id, - "pipeline_run_id": pipeline_run_id, - } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == {"events": events} + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline_debug/get", + "pipeline_id": pipeline_id, + "pipeline_run_id": pipeline_run_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"events": events} async def test_pipeline_debug_list_runs_wrong_pipeline( @@ -1787,94 +1799,97 @@ async def test_audio_pipeline_with_enhancements( events = [] client = await hass_ws_client(hass) - await client.send_json_auto_id( - { - "type": "assist_pipeline/run", - "start_stage": "stt", - "end_stage": "tts", - "input": { - "sample_rate": SAMPLE_RATE, - # Enhancements - "noise_suppression_level": 2, - "auto_gain_dbfs": 15, - "volume_multiplier": 2.0, - }, - } - ) + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + await client.send_json_auto_id( + { + "type": "assist_pipeline/run", + "start_stage": "stt", + "end_stage": "tts", + "input": { + "sample_rate": SAMPLE_RATE, + # Enhancements + "noise_suppression_level": 2, + "auto_gain_dbfs": 15, + "volume_multiplier": 2.0, + }, + } + ) - # result - msg = await client.receive_json() - assert msg["success"] + # result + msg = await client.receive_json() + assert msg["success"] - # run start - msg = await client.receive_json() - assert msg["event"]["type"] == "run-start" - msg["event"]["data"]["pipeline"] = ANY - assert msg["event"]["data"] == snapshot - handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] - events.append(msg["event"]) + # run start + msg = await client.receive_json() + assert msg["event"]["type"] == "run-start" + msg["event"]["data"]["pipeline"] = ANY + assert msg["event"]["data"] == snapshot + handler_id = msg["event"]["data"]["runner_data"]["stt_binary_handler_id"] + events.append(msg["event"]) - # stt - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # stt + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # One second of silence. - # This will pass through the audio enhancement pipeline, but we don't test - # the actual output. - await client.send_bytes(bytes([handler_id]) + bytes(BYTES_ONE_SECOND)) + # One second of silence. + # This will pass through the audio enhancement pipeline, but we don't test + # the actual output. + await client.send_bytes(bytes([handler_id]) + bytes(BYTES_ONE_SECOND)) - # End of audio stream (handler id + empty payload) - await client.send_bytes(bytes([handler_id])) + # End of audio stream (handler id + empty payload) + await client.send_bytes(bytes([handler_id])) - msg = await client.receive_json() - assert msg["event"]["type"] == "stt-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "stt-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # intent - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # intent + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "intent-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "intent-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # text-to-speech - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-start" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # text-to-speech + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-start" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - msg = await client.receive_json() - assert msg["event"]["type"] == "tts-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + msg = await client.receive_json() + assert msg["event"]["type"] == "tts-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - # run end - msg = await client.receive_json() - assert msg["event"]["type"] == "run-end" - assert msg["event"]["data"] == snapshot - events.append(msg["event"]) + # run end + msg = await client.receive_json() + assert msg["event"]["type"] == "run-end" + assert msg["event"]["data"] == snapshot + events.append(msg["event"]) - pipeline_data: PipelineData = hass.data[DOMAIN] - pipeline_id = list(pipeline_data.pipeline_debug)[0] - pipeline_run_id = list(pipeline_data.pipeline_debug[pipeline_id])[0] + pipeline_data: PipelineData = hass.data[DOMAIN] + pipeline_id = list(pipeline_data.pipeline_debug)[0] + pipeline_run_id = list(pipeline_data.pipeline_debug[pipeline_id])[0] - await client.send_json_auto_id( - { - "type": "assist_pipeline/pipeline_debug/get", - "pipeline_id": pipeline_id, - "pipeline_run_id": pipeline_run_id, - } - ) - msg = await client.receive_json() - assert msg["success"] - assert msg["result"] == {"events": events} + await client.send_json_auto_id( + { + "type": "assist_pipeline/pipeline_debug/get", + "pipeline_id": pipeline_id, + "pipeline_run_id": pipeline_run_id, + } + ) + msg = await client.receive_json() + assert msg["success"] + assert msg["result"] == {"events": events} async def test_wake_word_cooldown_same_id( diff --git a/tests/components/cloud/test_tts.py b/tests/components/cloud/test_tts.py index 499981c643d005..bf9fd7302ae777 100644 --- a/tests/components/cloud/test_tts.py +++ b/tests/components/cloud/test_tts.py @@ -227,25 +227,21 @@ async def test_get_tts_audio( await on_start_callback() 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_6e8b81ac47_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" - ), - } - await hass.async_block_till_done() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + 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/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() assert mock_process_tts.call_count == 1 assert mock_process_tts.call_args is not None @@ -280,25 +276,21 @@ async def test_get_tts_audio_logged_out( await hass.async_block_till_done() client = await hass_client() - url = "/api/tts_get_url" - data |= {"message": "There is someone at the door."} + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + 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() + 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_6e8b81ac47_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_6e8b81ac47_{expected_url_suffix}.mp3" - ), - } - await hass.async_block_till_done() + assert response == { + "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() assert mock_process_tts.call_count == 1 assert mock_process_tts.call_args is not None @@ -342,28 +334,24 @@ async def test_tts_entity( assert state assert state.state == STATE_UNKNOWN - url = "/api/tts_get_url" - data = { - "engine_id": entity_id, - "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_6e8b81ac47_{entity_id}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_6e8b81ac47_{entity_id}.mp3" - ), - } - await hass.async_block_till_done() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + url = "/api/tts_get_url" + data = { + "engine_id": entity_id, + "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/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() assert mock_process_tts.call_count == 1 assert mock_process_tts.call_args is not None @@ -482,29 +470,25 @@ async def test_deprecated_voice( client = await hass_client() # Test with non deprecated voice. - url = "/api/tts_get_url" - data |= { - "message": "There is someone at the door.", - "language": language, - "options": {"voice": replacement_voice}, - } - - 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"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_87567e3e29_{expected_url_suffix}.mp3" - ), - } - await hass.async_block_till_done() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + url = "/api/tts_get_url" + data |= { + "message": "There is someone at the door.", + "language": language, + "options": {"voice": replacement_voice}, + } + + 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/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() assert mock_process_tts.call_count == 1 assert mock_process_tts.call_args is not None @@ -522,22 +506,18 @@ async def test_deprecated_voice( # Test with deprecated voice. data["options"] = {"voice": deprecated_voice} - req = await client.post(url, json=data) - assert req.status == HTTPStatus.OK - response = await req.json() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + 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"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_13646b7d32_{expected_url_suffix}.mp3" - ), - } - await hass.async_block_till_done() + assert response == { + "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() issue_id = f"deprecated_voice_{deprecated_voice}" @@ -631,28 +611,24 @@ async def test_deprecated_gender( client = await hass_client() # Test without deprecated gender option. - url = "/api/tts_get_url" - data |= { - "message": "There is someone at the door.", - "language": language, - } - - 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"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_6e8b81ac47_{expected_url_suffix}.mp3" - ), - } - await hass.async_block_till_done() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + url = "/api/tts_get_url" + data |= { + "message": "There is someone at the door.", + "language": language, + } + + 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/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() assert mock_process_tts.call_count == 1 assert mock_process_tts.call_args is not None @@ -667,22 +643,18 @@ async def test_deprecated_gender( # Test with deprecated gender option. data["options"] = {"gender": gender_option} - 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"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_{language.lower()}_dd0e95eb04_{expected_url_suffix}.mp3" - ), - } - await hass.async_block_till_done() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + 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/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } + await hass.async_block_till_done() issue_id = "deprecated_gender" diff --git a/tests/components/tts/test_init.py b/tests/components/tts/test_init.py index 2ab6dc166296f9..80dff87eb9bf43 100644 --- a/tests/components/tts/test_init.py +++ b/tests/components/tts/test_init.py @@ -204,18 +204,20 @@ async def test_service( blocking=True, ) - assert len(calls) == 1 - assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True - assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_-_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_{expected_url_suffix}.mp3" - ).is_file() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert len(calls) == 1 + assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True + assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_{expected_url_suffix}.mp3" + ).is_file() @pytest.mark.parametrize( @@ -266,17 +268,20 @@ async def test_service_default_language( ) assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_-_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / ( - f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de-de_-_{expected_url_suffix}.mp3" - ) - ).is_file() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / ( + f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de-de_-_{expected_url_suffix}.mp3" + ) + ).is_file() @pytest.mark.parametrize( @@ -327,15 +332,18 @@ async def test_service_default_special_language( ) assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_-_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_{expected_url_suffix}.mp3" - ).is_file() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_{expected_url_suffix}.mp3" + ).is_file() @pytest.mark.parametrize( @@ -384,15 +392,18 @@ async def test_service_language( ) assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_-_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de-de_-_{expected_url_suffix}.mp3" - ).is_file() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / f"42f18378fd4393d18c8dd11d03fa9563c1e54491_de-de_-_{expected_url_suffix}.mp3" + ).is_file() @pytest.mark.parametrize( @@ -497,18 +508,21 @@ async def test_service_options( assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / ( - "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" - ) - ).is_file() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / ( + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" + ) + ).is_file() class MockProviderWithDefaults(MockTTSProvider): @@ -578,18 +592,21 @@ async def test_service_default_options( assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / ( - "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" - ) - ).is_file() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / ( + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" + ) + ).is_file() @pytest.mark.parametrize( @@ -649,18 +666,21 @@ async def test_merge_default_service_options( assert len(calls) == 1 assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" - ) - await hass.async_block_till_done() - assert ( - mock_tts_cache_dir - / ( - "42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" - ) - ).is_file() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() + assert ( + mock_tts_cache_dir + / ( + "42f18378fd4393d18c8dd11d03fa9563c1e54491" + f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" + ) + ).is_file() @pytest.mark.parametrize( @@ -1065,10 +1085,14 @@ async def test_setup_legacy_cache_dir( ) assert len(calls) == 1 - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_test.mp3" - ) - await hass.async_block_till_done() + + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() @pytest.mark.parametrize("mock_tts_entity", [MockEntityBoom(DEFAULT_LANG)]) @@ -1100,10 +1124,13 @@ async def test_setup_cache_dir( ) assert len(calls) == 1 - assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" - ) - await hass.async_block_till_done() + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + assert await get_media_source_url( + hass, calls[0].data[ATTR_MEDIA_CONTENT_ID] + ) == ("/api/tts_proxy/test_token.mp3") + await hass.async_block_till_done() class MockProviderEmpty(MockTTSProvider): @@ -1176,13 +1203,13 @@ async def test_service_get_tts_error( ) -async def test_load_cache_legacy_retrieve_without_mem_cache( +async def test_legacy_cannot_retrieve_without_token( hass: HomeAssistant, mock_provider: MockTTSProvider, mock_tts_cache_dir: Path, hass_client: ClientSessionGenerator, ) -> None: - """Set up component and load cache and get without mem cache.""" + """Verify that a TTS cannot be retrieved by filename directly.""" tts_data = b"" cache_file = ( mock_tts_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_test.mp3" @@ -1196,17 +1223,16 @@ async def test_load_cache_legacy_retrieve_without_mem_cache( url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_test.mp3" req = await client.get(url) - assert req.status == HTTPStatus.OK - assert await req.read() == tts_data + assert req.status == HTTPStatus.NOT_FOUND -async def test_load_cache_retrieve_without_mem_cache( +async def test_cannot_retrieve_without_token( hass: HomeAssistant, mock_tts_entity: MockTTSEntity, mock_tts_cache_dir: Path, hass_client: ClientSessionGenerator, ) -> None: - """Set up component and load cache and get without mem cache.""" + """Verify that a TTS cannot be retrieved by filename directly.""" tts_data = b"" cache_file = mock_tts_cache_dir / ( "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" @@ -1220,45 +1246,37 @@ async def test_load_cache_retrieve_without_mem_cache( url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" req = await client.get(url) - assert req.status == HTTPStatus.OK - assert await req.read() == tts_data + assert req.status == HTTPStatus.NOT_FOUND @pytest.mark.parametrize( - ("setup", "data", "expected_url_suffix"), + ("setup", "data"), [ - ("mock_setup", {"platform": "test"}, "test"), - ("mock_setup", {"engine_id": "test"}, "test"), - ("mock_config_entry_setup", {"engine_id": "tts.test"}, "tts.test"), + ("mock_setup", {"platform": "test"}), + ("mock_setup", {"engine_id": "test"}), + ("mock_config_entry_setup", {"engine_id": "tts.test"}), ], indirect=["setup"], ) async def test_web_get_url( - hass_client: ClientSessionGenerator, - setup: str, - data: dict[str, Any], - expected_url_suffix: str, + hass_client: ClientSessionGenerator, setup: str, data: dict[str, Any] ) -> None: """Set up a TTS platform and receive file from web.""" 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_-_{expected_url_suffix}.mp3" - ), - "path": ( - "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" - f"_en-us_-_{expected_url_suffix}.mp3" - ), - } + with patch( + "homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token" + ): + 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/test_token.mp3"), + "path": ("/api/tts_proxy/test_token.mp3"), + } @pytest.mark.parametrize(