Hide TTS filename behind random token (#131192)

* Hide TTS filename behind random token

* Clean up and fix test snapshots

* Fix tests

* Fix cloud tests
This commit is contained in:
Michael Hansen 2024-11-24 19:52:21 -06:00 committed by GitHub
parent cb4636ada1
commit d4071e7123
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 694 additions and 657 deletions

View File

@ -13,6 +13,7 @@ import logging
import mimetypes import mimetypes
import os import os
import re import re
import secrets
import subprocess import subprocess
import tempfile import tempfile
from typing import Any, Final, TypedDict, final from typing import Any, Final, TypedDict, final
@ -540,6 +541,10 @@ class SpeechManager:
self.file_cache: dict[str, str] = {} self.file_cache: dict[str, str] = {}
self.mem_cache: dict[str, TTSCache] = {} 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]: def _init_cache(self) -> dict[str, str]:
"""Init cache folder and fetch files.""" """Init cache folder and fetch files."""
try: try:
@ -656,7 +661,17 @@ class SpeechManager:
engine_instance, cache_key, message, use_cache, language, options 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( async def async_get_tts_audio(
self, self,
@ -910,11 +925,15 @@ class SpeechManager:
), ),
) )
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. """Read a voice file and return binary.
This method is a coroutine. 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 ( if not (record := _RE_VOICE_FILE.match(filename.lower())) and not (
record := _RE_LEGACY_VOICE_FILE.match(filename.lower()) record := _RE_LEGACY_VOICE_FILE.match(filename.lower())
): ):
@ -1076,6 +1095,7 @@ class TextToSpeechView(HomeAssistantView):
async def get(self, request: web.Request, filename: str) -> web.Response: async def get(self, request: web.Request, filename: str) -> web.Response:
"""Start a get request.""" """Start a get request."""
try: try:
# filename is actually token, but we keep its name for compatibility
content, data = await self.tts.async_read_tts(filename) content, data = await self.tts.async_read_tts(filename)
except HomeAssistantError as err: except HomeAssistantError as err:
_LOGGER.error("Error on load tts: %s", err) _LOGGER.error("Error on load tts: %s", err)

View File

@ -77,7 +77,7 @@
'tts_output': dict({ '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", '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', 'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', 'url': '/api/tts_proxy/test_token.mp3',
}), }),
}), }),
'type': <PipelineEventType.TTS_END: 'tts-end'>, 'type': <PipelineEventType.TTS_END: 'tts-end'>,
@ -166,7 +166,7 @@
'tts_output': dict({ '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", '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', 'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_2657c1a8ee_test.mp3', 'url': '/api/tts_proxy/test_token.mp3',
}), }),
}), }),
'type': <PipelineEventType.TTS_END: 'tts-end'>, 'type': <PipelineEventType.TTS_END: 'tts-end'>,
@ -255,7 +255,7 @@
'tts_output': dict({ '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", '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', 'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_2657c1a8ee_test.mp3', 'url': '/api/tts_proxy/test_token.mp3',
}), }),
}), }),
'type': <PipelineEventType.TTS_END: 'tts-end'>, 'type': <PipelineEventType.TTS_END: 'tts-end'>,
@ -368,7 +368,7 @@
'tts_output': dict({ '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", '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', 'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', 'url': '/api/tts_proxy/test_token.mp3',
}), }),
}), }),
'type': <PipelineEventType.TTS_END: 'tts-end'>, 'type': <PipelineEventType.TTS_END: 'tts-end'>,

View File

@ -73,7 +73,7 @@
'tts_output': dict({ '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", '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', '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({ '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", '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', '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({ '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", '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', '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({ '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", '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', 'mime_type': 'audio/mpeg',
'url': '/api/tts_proxy/dae2cdcb27a1d1c3b07ba2c7db91480f9d4bfd8f_en-us_031e2ec052_test.mp3', 'url': '/api/tts_proxy/test_token.mp3',
}), }),
}) })
# --- # ---

View File

@ -70,6 +70,9 @@ async def test_pipeline_from_audio_stream_auto(
yield make_10ms_chunk(b"part2") yield make_10ms_chunk(b"part2")
yield b"" yield b""
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await assist_pipeline.async_pipeline_from_audio_stream( await assist_pipeline.async_pipeline_from_audio_stream(
hass, hass,
context=Context(), context=Context(),
@ -133,6 +136,9 @@ async def test_pipeline_from_audio_stream_legacy(
assert msg["success"] assert msg["success"]
pipeline_id = msg["result"]["id"] pipeline_id = msg["result"]["id"]
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
# Use the created pipeline # Use the created pipeline
await assist_pipeline.async_pipeline_from_audio_stream( await assist_pipeline.async_pipeline_from_audio_stream(
hass, hass,
@ -198,6 +204,9 @@ async def test_pipeline_from_audio_stream_entity(
assert msg["success"] assert msg["success"]
pipeline_id = msg["result"]["id"] pipeline_id = msg["result"]["id"]
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
# Use the created pipeline # Use the created pipeline
await assist_pipeline.async_pipeline_from_audio_stream( await assist_pipeline.async_pipeline_from_audio_stream(
hass, hass,
@ -362,6 +371,9 @@ async def test_pipeline_from_audio_stream_wake_word(
yield b"" yield b""
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await assist_pipeline.async_pipeline_from_audio_stream( await assist_pipeline.async_pipeline_from_audio_stream(
hass, hass,
context=Context(), context=Context(),

View File

@ -119,6 +119,9 @@ async def test_audio_pipeline(
events = [] events = []
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "assist_pipeline/run", "type": "assist_pipeline/run",
@ -210,6 +213,9 @@ async def test_audio_pipeline_with_wake_word_timeout(
events = [] events = []
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "assist_pipeline/run", "type": "assist_pipeline/run",
@ -265,6 +271,9 @@ async def test_audio_pipeline_with_wake_word_no_timeout(
events = [] events = []
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "assist_pipeline/run", "type": "assist_pipeline/run",
@ -1540,6 +1549,9 @@ async def test_audio_pipeline_debug(
events = [] events = []
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "assist_pipeline/run", "type": "assist_pipeline/run",
@ -1787,6 +1799,9 @@ async def test_audio_pipeline_with_enhancements(
events = [] events = []
client = await hass_ws_client(hass) client = await hass_ws_client(hass)
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
await client.send_json_auto_id( await client.send_json_auto_id(
{ {
"type": "assist_pipeline/run", "type": "assist_pipeline/run",

View File

@ -227,6 +227,9 @@ async def test_get_tts_audio(
await on_start_callback() await on_start_callback()
client = await hass_client() client = await hass_client()
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
url = "/api/tts_get_url" url = "/api/tts_get_url"
data |= {"message": "There is someone at the door."} data |= {"message": "There is someone at the door."}
@ -235,15 +238,8 @@ async def test_get_tts_audio(
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()
@ -280,6 +276,9 @@ async def test_get_tts_audio_logged_out(
await hass.async_block_till_done() await hass.async_block_till_done()
client = await hass_client() client = await hass_client()
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
url = "/api/tts_get_url" url = "/api/tts_get_url"
data |= {"message": "There is someone at the door."} data |= {"message": "There is someone at the door."}
@ -288,15 +287,8 @@ async def test_get_tts_audio_logged_out(
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()
@ -342,6 +334,9 @@ async def test_tts_entity(
assert state assert state
assert state.state == STATE_UNKNOWN assert state.state == STATE_UNKNOWN
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
url = "/api/tts_get_url" url = "/api/tts_get_url"
data = { data = {
"engine_id": entity_id, "engine_id": entity_id,
@ -353,15 +348,8 @@ async def test_tts_entity(
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()
@ -482,6 +470,9 @@ async def test_deprecated_voice(
client = await hass_client() client = await hass_client()
# Test with non deprecated voice. # Test with non deprecated voice.
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
url = "/api/tts_get_url" url = "/api/tts_get_url"
data |= { data |= {
"message": "There is someone at the door.", "message": "There is someone at the door.",
@ -494,15 +485,8 @@ async def test_deprecated_voice(
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()
@ -522,20 +506,16 @@ async def test_deprecated_voice(
# Test with deprecated voice. # Test with deprecated voice.
data["options"] = {"voice": deprecated_voice} data["options"] = {"voice": deprecated_voice}
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
req = await client.post(url, json=data) req = await client.post(url, json=data)
assert req.status == HTTPStatus.OK assert req.status == HTTPStatus.OK
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()
@ -631,6 +611,9 @@ async def test_deprecated_gender(
client = await hass_client() client = await hass_client()
# Test without deprecated gender option. # Test without deprecated gender option.
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
url = "/api/tts_get_url" url = "/api/tts_get_url"
data |= { data |= {
"message": "There is someone at the door.", "message": "There is someone at the door.",
@ -642,15 +625,8 @@ async def test_deprecated_gender(
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()
@ -667,20 +643,16 @@ async def test_deprecated_gender(
# Test with deprecated gender option. # Test with deprecated gender option.
data["options"] = {"gender": gender_option} data["options"] = {"gender": gender_option}
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
req = await client.post(url, json=data) req = await client.post(url, json=data)
assert req.status == HTTPStatus.OK assert req.status == HTTPStatus.OK
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"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() await hass.async_block_till_done()

View File

@ -204,13 +204,15 @@ async def test_service(
blocking=True, blocking=True,
) )
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True assert calls[0].data[ATTR_MEDIA_ANNOUNCE] is True
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC
assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( assert await get_media_source_url(
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491" hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]
f"_en-us_-_{expected_url_suffix}.mp3" ) == ("/api/tts_proxy/test_token.mp3")
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -266,10 +268,13 @@ async def test_service_default_language(
) )
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC 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" with patch(
f"_de-de_-_{expected_url_suffix}.mp3" "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() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -327,10 +332,13 @@ async def test_service_default_special_language(
) )
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC 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" with patch(
f"_en-us_-_{expected_url_suffix}.mp3" "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() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -384,10 +392,13 @@ async def test_service_language(
) )
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC 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" with patch(
f"_de-de_-_{expected_url_suffix}.mp3" "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() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -497,10 +508,13 @@ async def test_service_options(
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC 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" with patch(
f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" "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() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -578,10 +592,13 @@ async def test_service_default_options(
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC 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" with patch(
f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" "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() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -649,10 +666,13 @@ async def test_merge_default_service_options(
assert len(calls) == 1 assert len(calls) == 1
assert calls[0].data[ATTR_MEDIA_CONTENT_TYPE] == MediaType.MUSIC 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" with patch(
f"_de-de_{opt_hash}_{expected_url_suffix}.mp3" "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() await hass.async_block_till_done()
assert ( assert (
mock_tts_cache_dir mock_tts_cache_dir
@ -1065,9 +1085,13 @@ async def test_setup_legacy_cache_dir(
) )
assert len(calls) == 1 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" 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() await hass.async_block_till_done()
@ -1100,9 +1124,12 @@ async def test_setup_cache_dir(
) )
assert len(calls) == 1 assert len(calls) == 1
assert await get_media_source_url(hass, calls[0].data[ATTR_MEDIA_CONTENT_ID]) == ( with patch(
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" "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() await hass.async_block_till_done()
@ -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, hass: HomeAssistant,
mock_provider: MockTTSProvider, mock_provider: MockTTSProvider,
mock_tts_cache_dir: Path, mock_tts_cache_dir: Path,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
) -> None: ) -> 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"" tts_data = b""
cache_file = ( cache_file = (
mock_tts_cache_dir / "42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_test.mp3" 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" url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en_-_test.mp3"
req = await client.get(url) req = await client.get(url)
assert req.status == HTTPStatus.OK assert req.status == HTTPStatus.NOT_FOUND
assert await req.read() == tts_data
async def test_load_cache_retrieve_without_mem_cache( async def test_cannot_retrieve_without_token(
hass: HomeAssistant, hass: HomeAssistant,
mock_tts_entity: MockTTSEntity, mock_tts_entity: MockTTSEntity,
mock_tts_cache_dir: Path, mock_tts_cache_dir: Path,
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator,
) -> None: ) -> 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"" tts_data = b""
cache_file = mock_tts_cache_dir / ( cache_file = mock_tts_cache_dir / (
"42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" "42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3"
@ -1220,28 +1246,27 @@ async def test_load_cache_retrieve_without_mem_cache(
url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3" url = "/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491_en-us_-_tts.test.mp3"
req = await client.get(url) req = await client.get(url)
assert req.status == HTTPStatus.OK assert req.status == HTTPStatus.NOT_FOUND
assert await req.read() == tts_data
@pytest.mark.parametrize( @pytest.mark.parametrize(
("setup", "data", "expected_url_suffix"), ("setup", "data"),
[ [
("mock_setup", {"platform": "test"}, "test"), ("mock_setup", {"platform": "test"}),
("mock_setup", {"engine_id": "test"}, "test"), ("mock_setup", {"engine_id": "test"}),
("mock_config_entry_setup", {"engine_id": "tts.test"}, "tts.test"), ("mock_config_entry_setup", {"engine_id": "tts.test"}),
], ],
indirect=["setup"], indirect=["setup"],
) )
async def test_web_get_url( async def test_web_get_url(
hass_client: ClientSessionGenerator, hass_client: ClientSessionGenerator, setup: str, data: dict[str, Any]
setup: str,
data: dict[str, Any],
expected_url_suffix: str,
) -> None: ) -> None:
"""Set up a TTS platform and receive file from web.""" """Set up a TTS platform and receive file from web."""
client = await hass_client() client = await hass_client()
with patch(
"homeassistant.components.tts.secrets.token_urlsafe", return_value="test_token"
):
url = "/api/tts_get_url" url = "/api/tts_get_url"
data |= {"message": "There is someone at the door."} data |= {"message": "There is someone at the door."}
@ -1249,15 +1274,8 @@ async def test_web_get_url(
assert req.status == HTTPStatus.OK assert req.status == HTTPStatus.OK
response = await req.json() response = await req.json()
assert response == { assert response == {
"url": ( "url": ("http://example.local:8123/api/tts_proxy/test_token.mp3"),
"http://example.local:8123/api/tts_proxy/" "path": ("/api/tts_proxy/test_token.mp3"),
"42f18378fd4393d18c8dd11d03fa9563c1e54491"
f"_en-us_-_{expected_url_suffix}.mp3"
),
"path": (
"/api/tts_proxy/42f18378fd4393d18c8dd11d03fa9563c1e54491"
f"_en-us_-_{expected_url_suffix}.mp3"
),
} }