Skip to content
Snippets Groups Projects
Unverified Commit aab676a3 authored by Joost Lekkerkerker's avatar Joost Lekkerkerker Committed by GitHub
Browse files

Add Overseerr service to get requests (#134229)

* Add service to get requests

* Add service to get requests

* Add service to get requests

* fix

* Add tests
parent 7f473b82
No related branches found
No related tags found
No related merge requests found
Showing
with 5460 additions and 14 deletions
...@@ -16,13 +16,24 @@ from homeassistant.components.webhook import ( ...@@ -16,13 +16,24 @@ from homeassistant.components.webhook import (
) )
from homeassistant.const import CONF_WEBHOOK_ID, Platform from homeassistant.const import CONF_WEBHOOK_ID, Platform
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.http import HomeAssistantView from homeassistant.helpers.http import HomeAssistantView
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, JSON_PAYLOAD, LOGGER, REGISTERED_NOTIFICATIONS from .const import DOMAIN, JSON_PAYLOAD, LOGGER, REGISTERED_NOTIFICATIONS
from .coordinator import OverseerrConfigEntry, OverseerrCoordinator from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
from .services import setup_services
PLATFORMS: list[Platform] = [Platform.SENSOR] PLATFORMS: list[Platform] = [Platform.SENSOR]
CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
"""Set up the Overseerr component."""
setup_services(hass)
return True
async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool: async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool:
"""Set up Overseerr from a config entry.""" """Set up Overseerr from a config entry."""
......
...@@ -9,6 +9,11 @@ LOGGER = logging.getLogger(__package__) ...@@ -9,6 +9,11 @@ LOGGER = logging.getLogger(__package__)
REQUESTS = "requests" REQUESTS = "requests"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_STATUS = "status"
ATTR_SORT_ORDER = "sort_order"
ATTR_REQUESTED_BY = "requested_by"
REGISTERED_NOTIFICATIONS = ( REGISTERED_NOTIFICATIONS = (
NotificationType.REQUEST_PENDING_APPROVAL NotificationType.REQUEST_PENDING_APPROVAL
| NotificationType.REQUEST_APPROVED | NotificationType.REQUEST_APPROVED
......
...@@ -23,5 +23,10 @@ ...@@ -23,5 +23,10 @@
"default": "mdi:message-bulleted" "default": "mdi:message-bulleted"
} }
} }
},
"services": {
"get_requests": {
"service": "mdi:multimedia"
}
} }
} }
rules: rules:
# Bronze # Bronze
action-setup: action-setup: done
status: exempt
comment: |
This integration does not provide additional actions.
appropriate-polling: done appropriate-polling: done
brands: done brands: done
common-modules: done common-modules: done
config-flow-test-coverage: done config-flow-test-coverage: done
config-flow: done config-flow: done
dependency-transparency: done dependency-transparency: done
docs-actions: docs-actions: done
status: exempt
comment: |
This integration does not provide additional actions.
docs-high-level-description: done docs-high-level-description: done
docs-installation-instructions: done docs-installation-instructions: done
docs-removal-instructions: done docs-removal-instructions: done
...@@ -29,10 +23,7 @@ rules: ...@@ -29,10 +23,7 @@ rules:
unique-config-entry: done unique-config-entry: done
# Silver # Silver
action-exceptions: action-exceptions: done
status: exempt
comment: |
This integration does not provide additional actions or actionable entities.
config-entry-unloading: done config-entry-unloading: done
docs-configuration-parameters: docs-configuration-parameters:
status: exempt status: exempt
......
"""Define services for the Overseerr integration."""
from dataclasses import asdict
from typing import Any, cast
from python_overseerr import OverseerrClient, OverseerrConnectionError
import voluptuous as vol
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import (
HomeAssistant,
ServiceCall,
ServiceResponse,
SupportsResponse,
)
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.util.json import JsonValueType
from .const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_REQUESTED_BY,
ATTR_SORT_ORDER,
ATTR_STATUS,
DOMAIN,
LOGGER,
)
from .coordinator import OverseerrConfigEntry
SERVICE_GET_REQUESTS = "get_requests"
SERVICE_GET_REQUESTS_SCHEMA = vol.Schema(
{
vol.Required(ATTR_CONFIG_ENTRY_ID): str,
vol.Optional(ATTR_STATUS): vol.In(
["approved", "pending", "available", "processing", "unavailable", "failed"]
),
vol.Optional(ATTR_SORT_ORDER): vol.In(["added", "modified"]),
vol.Optional(ATTR_REQUESTED_BY): int,
}
)
def async_get_entry(hass: HomeAssistant, config_entry_id: str) -> OverseerrConfigEntry:
"""Get the Overseerr config entry."""
if not (entry := hass.config_entries.async_get_entry(config_entry_id)):
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="integration_not_found",
translation_placeholders={"target": DOMAIN},
)
if entry.state is not ConfigEntryState.LOADED:
raise ServiceValidationError(
translation_domain=DOMAIN,
translation_key="not_loaded",
translation_placeholders={"target": entry.title},
)
return cast(OverseerrConfigEntry, entry)
async def get_media(
client: OverseerrClient, media_type: str, identifier: int
) -> dict[str, Any]:
"""Get media details."""
media = {}
try:
if media_type == "movie":
media = asdict(await client.get_movie_details(identifier))
if media_type == "tv":
media = asdict(await client.get_tv_details(identifier))
except OverseerrConnectionError:
LOGGER.error("Could not find data for %s %s", media_type, identifier)
return {}
media["media_info"].pop("requests")
return media
def setup_services(hass: HomeAssistant) -> None:
"""Set up the services for the Overseerr integration."""
async def async_get_requests(call: ServiceCall) -> ServiceResponse:
"""Get requests made to Overseerr."""
entry = async_get_entry(hass, call.data[ATTR_CONFIG_ENTRY_ID])
client = entry.runtime_data.client
kwargs: dict[str, Any] = {}
if status := call.data.get(ATTR_STATUS):
kwargs["status"] = status
if sort_order := call.data.get(ATTR_SORT_ORDER):
kwargs["sort"] = sort_order
if requested_by := call.data.get(ATTR_REQUESTED_BY):
kwargs["requested_by"] = requested_by
try:
requests = await client.get_requests(**kwargs)
except OverseerrConnectionError as err:
raise HomeAssistantError(
translation_domain=DOMAIN,
translation_key="connection_error",
translation_placeholders={"error": str(err)},
) from err
result: list[dict[str, Any]] = []
for request in requests:
req = asdict(request)
assert request.media.tmdb_id
req["media"] = await get_media(
client, request.media.media_type, request.media.tmdb_id
)
result.append(req)
return {"requests": cast(list[JsonValueType], result)}
hass.services.async_register(
DOMAIN,
SERVICE_GET_REQUESTS,
async_get_requests,
schema=SERVICE_GET_REQUESTS_SCHEMA,
supports_response=SupportsResponse.ONLY,
)
get_requests:
fields:
config_entry_id:
required: true
selector:
config_entry:
integration: overseerr
status:
selector:
select:
options:
- approved
- pending
- available
- processing
- unavailable
- failed
translation_key: request_status
sort_order:
selector:
select:
options:
- added
- modified
translation_key: request_sort_order
requested_by:
selector:
number:
min: 0
mode: box
...@@ -48,6 +48,54 @@ ...@@ -48,6 +48,54 @@
"exceptions": { "exceptions": {
"connection_error": { "connection_error": {
"message": "Error connecting to the Overseerr instance: {error}" "message": "Error connecting to the Overseerr instance: {error}"
},
"not_loaded": {
"message": "{target} is not loaded."
},
"integration_not_found": {
"message": "Integration \"{target}\" not found in registry."
}
},
"services": {
"get_requests": {
"name": "Get requests",
"description": "Get media requests from Overseerr.",
"fields": {
"config_entry_id": {
"name": "Overseerr instance",
"description": "The Overseerr instance to get requests from."
},
"status": {
"name": "Request status",
"description": "Filter the requests by status."
},
"sort_order": {
"name": "Sort order",
"description": "Sort the requests by added or modified date."
},
"requested_by": {
"name": "Requested by",
"description": "Filter the requests by the user id that requested them."
}
}
}
},
"selector": {
"request_status": {
"options": {
"approved": "Approved",
"pending": "Pending",
"available": "Available",
"processing": "Processing",
"unavailable": "Unavailable",
"failed": "Failed"
}
},
"request_sort_order": {
"options": {
"added": "Added",
"modified": "Modified"
}
} }
} }
} }
...@@ -4,8 +4,8 @@ from collections.abc import Generator ...@@ -4,8 +4,8 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
import pytest import pytest
from python_overseerr import RequestCount from python_overseerr import MovieDetails, RequestCount, RequestResponse
from python_overseerr.models import WebhookNotificationConfig from python_overseerr.models import TVDetails, WebhookNotificationConfig
from homeassistant.components.overseerr.const import DOMAIN from homeassistant.components.overseerr.const import DOMAIN
from homeassistant.const import ( from homeassistant.const import (
...@@ -54,6 +54,15 @@ def mock_overseerr_client() -> Generator[AsyncMock]: ...@@ -54,6 +54,15 @@ def mock_overseerr_client() -> Generator[AsyncMock]:
) )
) )
client.test_webhook_notification_config.return_value = True client.test_webhook_notification_config.return_value = True
client.get_requests.return_value = RequestResponse.from_json(
load_fixture("requests.json", DOMAIN)
).results
client.get_movie_details.return_value = MovieDetails.from_json(
load_fixture("movie.json", DOMAIN)
)
client.get_tv_details.return_value = TVDetails.from_json(
load_fixture("tv.json", DOMAIN)
)
yield client yield client
......
This diff is collapsed.
{
"pageInfo": {
"pages": 2,
"pageSize": 10,
"results": 14,
"page": 1
},
"results": [
{
"id": 16,
"status": 2,
"createdAt": "2024-12-29T10:04:16.000Z",
"updatedAt": "2024-12-29T10:04:16.000Z",
"type": "movie",
"is4k": false,
"serverId": 0,
"profileId": 7,
"rootFolder": "/media/movies",
"languageProfileId": null,
"tags": [],
"isAutoRequest": false,
"media": {
"downloadStatus": [],
"downloadStatus4k": [],
"id": 537,
"mediaType": "movie",
"tmdbId": 1156593,
"tvdbId": null,
"imdbId": null,
"status": 3,
"status4k": 1,
"createdAt": "2024-12-29T10:04:16.000Z",
"updatedAt": "2024-12-29T10:04:17.000Z",
"lastSeasonChange": "2024-12-29T10:04:16.000Z",
"mediaAddedAt": null,
"serviceId": 0,
"serviceId4k": null,
"externalServiceId": 423,
"externalServiceId4k": null,
"externalServiceSlug": "1156593",
"externalServiceSlug4k": null,
"ratingKey": null,
"ratingKey4k": null,
"serviceUrl": "http://192.168.0.1:7878/movie/1156593"
},
"seasons": [],
"modifiedBy": {
"permissions": 2,
"id": 1,
"email": "one@email.com",
"plexUsername": "somebody",
"username": null,
"recoveryLinkExpirationDate": null,
"userType": 1,
"plexId": 321321321,
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
"movieQuotaLimit": null,
"movieQuotaDays": null,
"tvQuotaLimit": null,
"tvQuotaDays": null,
"createdAt": "2024-12-16T21:13:58.000Z",
"updatedAt": "2024-12-16T23:59:03.000Z",
"requestCount": 11,
"displayName": "somebody"
},
"requestedBy": {
"permissions": 2,
"id": 1,
"email": "one@email.com",
"plexUsername": "somebody",
"username": null,
"recoveryLinkExpirationDate": null,
"userType": 1,
"plexId": 321321321,
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
"movieQuotaLimit": null,
"movieQuotaDays": null,
"tvQuotaLimit": null,
"tvQuotaDays": null,
"createdAt": "2024-12-16T21:13:58.000Z",
"updatedAt": "2024-12-16T23:59:03.000Z",
"requestCount": 11,
"displayName": "somebody"
},
"seasonCount": 0
},
{
"id": 14,
"status": 2,
"createdAt": "2024-12-26T14:37:30.000Z",
"updatedAt": "2024-12-26T14:37:30.000Z",
"type": "tv",
"is4k": false,
"serverId": 0,
"profileId": 7,
"rootFolder": "/media/tv",
"languageProfileId": 1,
"tags": [],
"isAutoRequest": false,
"media": {
"downloadStatus": [],
"downloadStatus4k": [],
"id": 535,
"mediaType": "tv",
"tmdbId": 249522,
"tvdbId": 447806,
"imdbId": null,
"status": 4,
"status4k": 1,
"createdAt": "2024-12-26T14:37:30.000Z",
"updatedAt": "2024-12-26T14:45:00.000Z",
"lastSeasonChange": "2024-12-26T14:37:30.000Z",
"mediaAddedAt": "2024-12-26T14:39:56.000Z",
"serviceId": 0,
"serviceId4k": null,
"externalServiceId": 144,
"externalServiceId4k": null,
"externalServiceSlug": "beast-games",
"externalServiceSlug4k": null,
"ratingKey": "10189",
"ratingKey4k": null,
"plexUrl": "https://app.plex.tv/desktop#!/server/aaaa/details?key=%2Flibrary%2Fmetadata%2F10189",
"iOSPlexUrl": "plex://preplay/?metadataKey=%2Flibrary%2Fmetadata%2F10189&server=aaaa",
"serviceUrl": "http://192.168.0.2:8989/series/beast-games"
},
"seasons": [
{
"id": 4,
"seasonNumber": 1,
"status": 2,
"createdAt": "2024-12-26T14:37:30.000Z",
"updatedAt": "2024-12-26T14:37:30.000Z"
}
],
"modifiedBy": {
"permissions": 2,
"id": 1,
"email": "one@email.com",
"plexUsername": "somebody",
"username": null,
"recoveryLinkExpirationDate": null,
"userType": 1,
"plexId": 321321321,
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
"movieQuotaLimit": null,
"movieQuotaDays": null,
"tvQuotaLimit": null,
"tvQuotaDays": null,
"createdAt": "2024-12-16T21:13:58.000Z",
"updatedAt": "2024-12-16T23:59:03.000Z",
"requestCount": 11,
"displayName": "somebody"
},
"requestedBy": {
"permissions": 2,
"id": 1,
"email": "one@email.com",
"plexUsername": "somebody",
"username": null,
"recoveryLinkExpirationDate": null,
"userType": 1,
"plexId": 321321321,
"avatar": "https://plex.tv/users/aaaaa/avatar?c=aaaaa",
"movieQuotaLimit": null,
"movieQuotaDays": null,
"tvQuotaLimit": null,
"tvQuotaDays": null,
"createdAt": "2024-12-16T21:13:58.000Z",
"updatedAt": "2024-12-16T23:59:03.000Z",
"requestCount": 11,
"displayName": "somebody"
},
"seasonCount": 1
}
]
}
This diff is collapsed.
# serializer version: 1
# name: test_service_get_requests
dict({
'requests': list([
dict({
'created_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc),
'id': 16,
'is4k': False,
'media': dict({
'adult': False,
'budget': 0,
'genres': list([
dict({
'id': 10749,
'name': 'Romance',
}),
dict({
'id': 18,
'name': 'Drama',
}),
]),
'id': 1156593,
'imdb_id': 'tt28510079',
'keywords': list([
dict({
'id': 818,
'name': 'based on novel or book',
}),
dict({
'id': 9663,
'name': 'sequel',
}),
]),
'media_info': dict({
'created_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc),
'id': 537,
'imdb_id': None,
'media_type': <MediaType.MOVIE: 'movie'>,
'status': <MediaStatus.PROCESSING: 3>,
'tmdb_id': 1156593,
'tvdb_id': None,
'updated_at': datetime.datetime(2024, 12, 29, 10, 4, 17, tzinfo=datetime.timezone.utc),
}),
'original_language': 'es',
'original_title': 'Culpa tuya',
'overview': "The love between Noah and Nick seems unwavering despite their parents' attempts to separate them. But his job and her entry into college open up their lives to new relationships that will shake the foundations of both their relationship and the Leister family itself.",
'popularity': 3958.479,
'release_date': datetime.date(2024, 12, 26),
'revenue': 0,
'runtime': 120,
'tagline': 'Divided by family. Driven by love.',
'title': 'Your Fault',
'vote_average': 7.7,
'vote_count': 190,
}),
'modified_by': dict({
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
'display_name': 'somebody',
'email': 'one@email.com',
'id': 1,
'movie_quota_days': None,
'movie_quota_limit': None,
'plex_id': 321321321,
'plex_username': 'somebody',
'request_count': 11,
'tv_quota_days': None,
'tv_quota_limit': None,
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
}),
'requested_by': dict({
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
'display_name': 'somebody',
'email': 'one@email.com',
'id': 1,
'movie_quota_days': None,
'movie_quota_limit': None,
'plex_id': 321321321,
'plex_username': 'somebody',
'request_count': 11,
'tv_quota_days': None,
'tv_quota_limit': None,
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
}),
'season_count': 0,
'status': <RequestStatus.APPROVED: 2>,
'updated_at': datetime.datetime(2024, 12, 29, 10, 4, 16, tzinfo=datetime.timezone.utc),
}),
dict({
'created_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc),
'id': 14,
'is4k': False,
'media': dict({
'first_air_date': datetime.date(2024, 12, 19),
'genres': list([
dict({
'id': 10764,
'name': 'Reality',
}),
]),
'id': 249522,
'keywords': list([
dict({
'id': 271,
'name': 'competition',
}),
dict({
'id': 4325,
'name': 'game show',
}),
dict({
'id': 330122,
'name': 'mrbeast',
}),
]),
'languages': list([
'da',
'en',
]),
'last_air_date': datetime.date(2024, 12, 26),
'last_episode_to_air': dict({
'air_date': datetime.date(2024, 12, 26),
'episode_number': 3,
'id': 5802152,
'name': 'The Solitary Experiment',
'overview': 'What would happen if three best friends were trapped in a room, but only two could escape? Watch and see for yourself right now!',
'still_path': '/r6LRRaA2l2tMDttWbYl3dXdJUij.jpg',
}),
'media_info': dict({
'created_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc),
'id': 535,
'imdb_id': None,
'media_type': <MediaType.TV: 'tv'>,
'status': <MediaStatus.PARTIALLY_AVAILABLE: 4>,
'tmdb_id': 249522,
'tvdb_id': 447806,
'updated_at': datetime.datetime(2024, 12, 26, 14, 45, tzinfo=datetime.timezone.utc),
}),
'name': 'Beast Games',
'next_episode_to_air': dict({
'air_date': datetime.date(2025, 1, 2),
'episode_number': 4,
'id': 5802153,
'name': 'Episode 4',
'overview': '',
'still_path': 'None',
}),
'number_of_episodes': 10,
'number_of_seasons': 1,
'original_language': 'en',
'original_name': 'Beast Games',
'overview': "I gathered 1,000 people to fight for $5,000,000, the LARGEST cash prize in TV history! We're also giving away a private island, Lamborghinis, and millions more in cash throughout the competition! Go watch to see the greatest show ever made!",
'popularity': 769.189,
'seasons': list([
dict({
'air_date': datetime.date(2024, 12, 19),
'episode_count': 10,
'id': 384427,
'name': 'Season 1',
'overview': '',
'poster_path': '/3itZlypnOcVcqI5xxyO6nvJ52yM.jpg',
'season_number': 1,
}),
]),
'tagline': '1,000 players. 5 million dollars. 1 winner.',
}),
'modified_by': dict({
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
'display_name': 'somebody',
'email': 'one@email.com',
'id': 1,
'movie_quota_days': None,
'movie_quota_limit': None,
'plex_id': 321321321,
'plex_username': 'somebody',
'request_count': 11,
'tv_quota_days': None,
'tv_quota_limit': None,
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
}),
'requested_by': dict({
'avatar': 'https://plex.tv/users/aaaaa/avatar?c=aaaaa',
'created_at': datetime.datetime(2024, 12, 16, 21, 13, 58, tzinfo=datetime.timezone.utc),
'display_name': 'somebody',
'email': 'one@email.com',
'id': 1,
'movie_quota_days': None,
'movie_quota_limit': None,
'plex_id': 321321321,
'plex_username': 'somebody',
'request_count': 11,
'tv_quota_days': None,
'tv_quota_limit': None,
'updated_at': datetime.datetime(2024, 12, 16, 23, 59, 3, tzinfo=datetime.timezone.utc),
}),
'season_count': 1,
'status': <RequestStatus.APPROVED: 2>,
'updated_at': datetime.datetime(2024, 12, 26, 14, 37, 30, tzinfo=datetime.timezone.utc),
}),
]),
})
# ---
"""Tests for the Overseerr services."""
from unittest.mock import AsyncMock
import pytest
from python_overseerr import OverseerrConnectionError
from syrupy import SnapshotAssertion
from homeassistant.components.overseerr.const import (
ATTR_CONFIG_ENTRY_ID,
ATTR_REQUESTED_BY,
ATTR_SORT_ORDER,
ATTR_STATUS,
DOMAIN,
)
from homeassistant.components.overseerr.services import SERVICE_GET_REQUESTS
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"] == {}
@pytest.mark.parametrize(
("service", "payload", "function", "exception", "raised_exception", "message"),
[
(
SERVICE_GET_REQUESTS,
{},
"get_requests",
OverseerrConnectionError("Timeout"),
HomeAssistantError,
"Error connecting to the Overseerr 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, {}),
],
)
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, match="Mock Title is not loaded"):
await hass.services.async_call(
DOMAIN,
service,
{ATTR_CONFIG_ENTRY_ID: mock_config_entry2.entry_id} | payload,
blocking=True,
return_response=True,
)
with pytest.raises(
ServiceValidationError, match='Integration "overseerr" not found in registry'
):
await hass.services.async_call(
DOMAIN,
service,
{ATTR_CONFIG_ENTRY_ID: "bad-config_id"} | payload,
blocking=True,
return_response=True,
)
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment