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
This commit is contained in:
Joost Lekkerkerker 2025-01-04 15:53:15 +01:00 committed by GitHub
parent 7f473b8260
commit aab676a313
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 5460 additions and 14 deletions

View File

@ -16,13 +16,24 @@ from homeassistant.components.webhook import (
)
from homeassistant.const import CONF_WEBHOOK_ID, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import config_validation as cv
from homeassistant.helpers.http import HomeAssistantView
from homeassistant.helpers.typing import ConfigType
from .const import DOMAIN, JSON_PAYLOAD, LOGGER, REGISTERED_NOTIFICATIONS
from .coordinator import OverseerrConfigEntry, OverseerrCoordinator
from .services import setup_services
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:
"""Set up Overseerr from a config entry."""

View File

@ -9,6 +9,11 @@ LOGGER = logging.getLogger(__package__)
REQUESTS = "requests"
ATTR_CONFIG_ENTRY_ID = "config_entry_id"
ATTR_STATUS = "status"
ATTR_SORT_ORDER = "sort_order"
ATTR_REQUESTED_BY = "requested_by"
REGISTERED_NOTIFICATIONS = (
NotificationType.REQUEST_PENDING_APPROVAL
| NotificationType.REQUEST_APPROVED

View File

@ -23,5 +23,10 @@
"default": "mdi:message-bulleted"
}
}
},
"services": {
"get_requests": {
"service": "mdi:multimedia"
}
}
}

View File

@ -1,19 +1,13 @@
rules:
# Bronze
action-setup:
status: exempt
comment: |
This integration does not provide additional actions.
action-setup: done
appropriate-polling: done
brands: done
common-modules: done
config-flow-test-coverage: done
config-flow: done
dependency-transparency: done
docs-actions:
status: exempt
comment: |
This integration does not provide additional actions.
docs-actions: done
docs-high-level-description: done
docs-installation-instructions: done
docs-removal-instructions: done
@ -29,10 +23,7 @@ rules:
unique-config-entry: done
# Silver
action-exceptions:
status: exempt
comment: |
This integration does not provide additional actions or actionable entities.
action-exceptions: done
config-entry-unloading: done
docs-configuration-parameters:
status: exempt

View File

@ -0,0 +1,115 @@
"""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,
)

View File

@ -0,0 +1,30 @@
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

View File

@ -48,6 +48,54 @@
"exceptions": {
"connection_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"
}
}
}
}

View File

@ -4,8 +4,8 @@ from collections.abc import Generator
from unittest.mock import AsyncMock, patch
import pytest
from python_overseerr import RequestCount
from python_overseerr.models import WebhookNotificationConfig
from python_overseerr import MovieDetails, RequestCount, RequestResponse
from python_overseerr.models import TVDetails, WebhookNotificationConfig
from homeassistant.components.overseerr.const import DOMAIN
from homeassistant.const import (
@ -54,6 +54,15 @@ def mock_overseerr_client() -> Generator[AsyncMock]:
)
)
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

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,176 @@
{
"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
}
]
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,204 @@
# 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),
}),
]),
})
# ---

View File

@ -0,0 +1,156 @@
"""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,
)