"""Test downloader services.""" import asyncio from contextlib import AbstractContextManager, nullcontext as does_not_raise import pytest import requests from requests_mock import Mocker import voluptuous as vol from homeassistant.components.downloader.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError @pytest.mark.usefixtures("setup_integration") @pytest.mark.parametrize( ("subdir", "expected_result"), [ ("test", does_not_raise()), ("test/path", does_not_raise()), ("~test/path", pytest.raises(ServiceValidationError)), ("~/../test/path", pytest.raises(ServiceValidationError)), ("../test/path", pytest.raises(ServiceValidationError)), (".../test/path", pytest.raises(ServiceValidationError)), ("/test/path", pytest.raises(ServiceValidationError)), ], ) async def test_download_invalid_subdir( hass: HomeAssistant, download_completed: asyncio.Event, download_failed: asyncio.Event, download_url: str, subdir: str, expected_result: AbstractContextManager, ) -> None: """Test service invalid subdirectory.""" async def call_service() -> None: """Call the download service.""" completed = hass.async_create_task(download_completed.wait()) failed = hass.async_create_task(download_failed.wait()) await hass.services.async_call( DOMAIN, "download_file", { "url": download_url, "subdir": subdir, "filename": "file.txt", "overwrite": True, }, blocking=True, ) await asyncio.wait((completed, failed), return_when=asyncio.FIRST_COMPLETED) with expected_result: await call_service() @pytest.mark.usefixtures("setup_integration") async def test_download_headers_passed_through( hass: HomeAssistant, requests_mock: Mocker, download_completed: asyncio.Event, download_url: str, ) -> None: """Test that custom headers are passed to the HTTP request.""" await hass.services.async_call( DOMAIN, "download_file", { "url": download_url, "headers": {"Authorization": "Bearer token123", "X-Custom": "value"}, }, blocking=True, ) await download_completed.wait() assert requests_mock.last_request.headers["Authorization"] == "Bearer token123" assert requests_mock.last_request.headers["X-Custom"] == "value" @pytest.mark.usefixtures("setup_integration") @pytest.mark.parametrize( ("headers", "expected_result"), [ (1, pytest.raises(vol.error.Invalid)), # Not a dictionary ({"Accept": "application/json"}, does_not_raise()), ({123: 456.789}, does_not_raise()), # Convert numbers to strings ( {"Accept": ["application/json"]}, pytest.raises(vol.error.MultipleInvalid), ), # Value is not a string ({1: None}, pytest.raises(vol.error.MultipleInvalid)), # Value is None ( {None: "application/json"}, pytest.raises(vol.error.MultipleInvalid), ), # Key is None ], ) async def test_download_headers_schema( hass: HomeAssistant, download_completed: asyncio.Event, download_failed: asyncio.Event, download_url: str, headers: dict[str, str], expected_result: AbstractContextManager, ) -> None: """Test service with headers.""" async def call_service() -> None: """Call the download service.""" completed = hass.async_create_task(download_completed.wait()) failed = hass.async_create_task(download_failed.wait()) await hass.services.async_call( DOMAIN, "download_file", { "url": download_url, "headers": headers, "subdir": "test", "filename": "file.txt", "overwrite": True, }, blocking=True, ) await asyncio.wait((completed, failed), return_when=asyncio.FIRST_COMPLETED) with expected_result: await call_service() @pytest.mark.usefixtures("setup_integration") @pytest.mark.parametrize( ("exception", "expected_error"), [ pytest.param( requests.exceptions.ConnectionError, HomeAssistantError, id="connection_error", ), pytest.param( ValueError, ServiceValidationError, id="value_error", ), ], ) async def test_download_exceptions( hass: HomeAssistant, requests_mock: Mocker, download_url: str, download_failed: asyncio.Event, exception: type[Exception], expected_error: type[Exception], ) -> None: """Test that exceptions during download propagate to the caller.""" requests_mock.get(download_url, exc=exception) with pytest.raises(expected_error): await hass.services.async_call( DOMAIN, "download_file", {"url": download_url}, blocking=True ) await download_failed.wait()