"""Tests for the Overseerr services.""" import dataclasses from unittest.mock import AsyncMock import pytest from python_overseerr import OverseerrConnectionError from python_overseerr.models import MediaType from syrupy.assertion import SnapshotAssertion from homeassistant.components.overseerr.const import ( ATTR_MEDIA_ID, ATTR_MEDIA_TYPE, ATTR_QUERY, ATTR_REQUESTED_BY, ATTR_SEASONS, ATTR_SORT_ORDER, ATTR_STATUS, DOMAIN, ) from homeassistant.components.overseerr.services import ( SERVICE_GET_REQUESTS, SERVICE_REQUEST_MEDIA, SERVICE_SEARCH_MEDIA, parse_seasons_input, ) from homeassistant.const import ATTR_CONFIG_ENTRY_ID from homeassistant.core import HomeAssistant from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from . import setup_integration from tests.common import MockConfigEntry async def test_service_get_requests( hass: HomeAssistant, mock_overseerr_client: AsyncMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test the get_requests service.""" await setup_integration(hass, mock_config_entry) response = await hass.services.async_call( DOMAIN, SERVICE_GET_REQUESTS, { ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, ATTR_STATUS: "approved", ATTR_SORT_ORDER: "added", ATTR_REQUESTED_BY: 1, }, blocking=True, return_response=True, ) assert response == snapshot for request in response["requests"]: assert "requests" not in request["media"]["media_info"] mock_overseerr_client.get_requests.assert_called_once_with( status="approved", sort="added", requested_by=1 ) async def test_service_get_requests_no_meta( hass: HomeAssistant, mock_overseerr_client: AsyncMock, mock_config_entry: MockConfigEntry, snapshot: SnapshotAssertion, ) -> None: """Test the get_requests service.""" mock_overseerr_client.get_movie_details.side_effect = OverseerrConnectionError mock_overseerr_client.get_tv_details.side_effect = OverseerrConnectionError await setup_integration(hass, mock_config_entry) response = await hass.services.async_call( DOMAIN, SERVICE_GET_REQUESTS, {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id}, blocking=True, return_response=True, ) for request in response["requests"]: assert request["media"] == {} async def test_service_search_media( hass: HomeAssistant, mock_overseerr_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test the search_media service.""" # Mock the search method mock_overseerr_client.search.return_value = [] await setup_integration(hass, mock_config_entry) # Test with a query containing spaces response = await hass.services.async_call( DOMAIN, SERVICE_SEARCH_MEDIA, { ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, ATTR_QUERY: "test query with spaces", }, blocking=True, return_response=True, ) assert response == {"results": []} mock_overseerr_client.search.assert_called_once_with("test query with spaces") async def test_service_request_media( hass: HomeAssistant, mock_overseerr_client: AsyncMock, mock_config_entry: MockConfigEntry, ) -> None: """Test the request_media service.""" # Mock the create request method @dataclasses.dataclass class RequestWithMediaMock: tmdb_id: str = "123456789" media_type: MediaType = MediaType.TV mock_overseerr_client.create_request.return_value = RequestWithMediaMock() await setup_integration(hass, mock_config_entry) response = await hass.services.async_call( DOMAIN, SERVICE_REQUEST_MEDIA, { ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id, ATTR_MEDIA_TYPE: "tv", ATTR_MEDIA_ID: "123456789", ATTR_SEASONS: "1", }, blocking=True, return_response=True, ) assert response == {"request": {"media_type": MediaType.TV, "tmdb_id": "123456789"}} @pytest.mark.parametrize( ("service", "payload", "function", "exception", "raised_exception", "message"), [ ( SERVICE_GET_REQUESTS, {}, "get_requests", OverseerrConnectionError("Timeout"), HomeAssistantError, "Error connecting to the Seerr instance: Timeout", ), ( SERVICE_SEARCH_MEDIA, {ATTR_QUERY: "test"}, "search", OverseerrConnectionError("Timeout"), HomeAssistantError, "Error connecting to the Seerr instance: Timeout", ), ( SERVICE_REQUEST_MEDIA, {ATTR_MEDIA_TYPE: "tv", ATTR_MEDIA_ID: "123456789", ATTR_SEASONS: "1"}, "create_request", OverseerrConnectionError("Timeout"), HomeAssistantError, "Error connecting to the Seerr instance: Timeout", ), ], ) async def test_services_connection_error( hass: HomeAssistant, mock_overseerr_client: AsyncMock, mock_config_entry: MockConfigEntry, service: str, payload: dict[str, str], function: str, exception: Exception, raised_exception: type[Exception], message: str, ) -> None: """Test a connection error in the services.""" await setup_integration(hass, mock_config_entry) getattr(mock_overseerr_client, function).side_effect = exception with pytest.raises(raised_exception, match=message): await hass.services.async_call( DOMAIN, service, {ATTR_CONFIG_ENTRY_ID: mock_config_entry.entry_id} | payload, blocking=True, return_response=True, ) @pytest.mark.parametrize( ("service", "payload"), [ (SERVICE_GET_REQUESTS, {}), (SERVICE_SEARCH_MEDIA, {ATTR_QUERY: "test"}), ( SERVICE_REQUEST_MEDIA, {ATTR_MEDIA_TYPE: "tv", ATTR_MEDIA_ID: "123456789", ATTR_SEASONS: "1"}, ), ], ) async def test_service_entry_availability( hass: HomeAssistant, mock_overseerr_client: AsyncMock, mock_config_entry: MockConfigEntry, service: str, payload: dict[str, str], ) -> None: """Test the services without valid entry.""" mock_config_entry.add_to_hass(hass) mock_config_entry2 = MockConfigEntry(domain=DOMAIN) mock_config_entry2.add_to_hass(hass) await hass.config_entries.async_setup(mock_config_entry.entry_id) await hass.async_block_till_done() with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( DOMAIN, service, {ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload, blocking=True, return_response=True, ) assert err.value.translation_key == "service_config_entry_not_loaded" with pytest.raises(ServiceValidationError) as err: await hass.services.async_call( DOMAIN, service, {ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload, blocking=True, return_response=True, ) assert err.value.translation_key == "service_config_entry_not_found" @pytest.mark.parametrize( ("seasons_input", "expected_seasons"), [ ("1", [1]), ("1,", [1]), ("1,2,3", [1, 2, 3]), ("1, 2, 3", [1, 2, 3]), (" 1 , 2, 3 ", [1, 2, 3]), ("[1]", [1]), ("[1,2,3]", [1, 2, 3]), ("[ 1 , 2 , 3]", [1, 2, 3]), ("", "all"), (" ", "all"), (None, "all"), ("all", "all"), ("Not a valid input", "all"), ("-", "all"), ], ) def test_parse_seasons_input( seasons_input: str | None, expected_seasons: list[int] | str ) -> None: """Test that all inputs are parsed correctly.""" assert expected_seasons == parse_seasons_input(seasons_input)