From 23ed62c1bc3e90859b9d013e83564654aed51513 Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 3 Jan 2025 08:26:01 +0100 Subject: [PATCH] Push Overseerr updates via webhook (#134187) --- .../components/overseerr/__init__.py | 85 ++++++++++++++- .../components/overseerr/config_flow.py | 11 +- homeassistant/components/overseerr/const.py | 37 +++++++ .../components/overseerr/manifest.json | 3 +- homeassistant/generated/integrations.json | 2 +- tests/components/overseerr/__init__.py | 26 +++++ tests/components/overseerr/conftest.py | 18 +++- tests/components/overseerr/const.py | 3 + .../overseerr/fixtures/webhook_config.json | 8 ++ ...ebhook_request_automatically_approved.json | 25 +++++ .../fixtures/webhook_test_notification.json | 12 +++ .../components/overseerr/test_config_flow.py | 25 ++++- tests/components/overseerr/test_init.py | 101 ++++++++++++++++++ tests/components/overseerr/test_sensor.py | 30 +++++- 14 files changed, 376 insertions(+), 10 deletions(-) create mode 100644 tests/components/overseerr/const.py create mode 100644 tests/components/overseerr/fixtures/webhook_config.json create mode 100644 tests/components/overseerr/fixtures/webhook_request_automatically_approved.json create mode 100644 tests/components/overseerr/fixtures/webhook_test_notification.json diff --git a/homeassistant/components/overseerr/__init__.py b/homeassistant/components/overseerr/__init__.py index 6d11dbc1fae..db39a0d3e92 100644 --- a/homeassistant/components/overseerr/__init__.py +++ b/homeassistant/components/overseerr/__init__.py @@ -2,9 +2,23 @@ from __future__ import annotations -from homeassistant.const import Platform -from homeassistant.core import HomeAssistant +import json +from aiohttp.hdrs import METH_POST +from aiohttp.web_request import Request +from aiohttp.web_response import Response +from python_overseerr import OverseerrConnectionError + +from homeassistant.components.webhook import ( + async_generate_url, + async_register, + async_unregister, +) +from homeassistant.const import CONF_WEBHOOK_ID, Platform +from homeassistant.core import HomeAssistant +from homeassistant.helpers.http import HomeAssistantView + +from .const import DOMAIN, JSON_PAYLOAD, LOGGER, REGISTERED_NOTIFICATIONS from .coordinator import OverseerrConfigEntry, OverseerrCoordinator PLATFORMS: list[Platform] = [Platform.SENSOR] @@ -19,6 +33,15 @@ async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> entry.runtime_data = coordinator + webhook_manager = OverseerrWebhookManager(hass, entry) + + try: + await webhook_manager.register_webhook() + except OverseerrConnectionError: + LOGGER.error("Failed to register Overseerr webhook") + + entry.async_on_unload(webhook_manager.unregister_webhook) + await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) return True @@ -27,3 +50,61 @@ async def async_setup_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> async def async_unload_entry(hass: HomeAssistant, entry: OverseerrConfigEntry) -> bool: """Unload a config entry.""" return await hass.config_entries.async_unload_platforms(entry, PLATFORMS) + + +class OverseerrWebhookManager: + """Overseerr webhook manager.""" + + def __init__(self, hass: HomeAssistant, entry: OverseerrConfigEntry) -> None: + """Initialize Overseerr webhook manager.""" + self.hass = hass + self.entry = entry + self.client = entry.runtime_data.client + + async def register_webhook(self) -> None: + """Register webhook.""" + async_register( + self.hass, + DOMAIN, + self.entry.title, + self.entry.data[CONF_WEBHOOK_ID], + self.handle_webhook, + allowed_methods=[METH_POST], + ) + url = async_generate_url(self.hass, self.entry.data[CONF_WEBHOOK_ID]) + if not await self.check_need_change(url): + return + LOGGER.debug("Setting Overseerr webhook to %s", url) + if not await self.client.test_webhook_notification_config(url, JSON_PAYLOAD): + LOGGER.debug("Failed to set Overseerr webhook") + return + await self.client.set_webhook_notification_config( + enabled=True, + types=REGISTERED_NOTIFICATIONS, + webhook_url=url, + json_payload=JSON_PAYLOAD, + ) + + async def check_need_change(self, url: str) -> bool: + """Check if webhook needs to be changed.""" + current_config = await self.client.get_webhook_notification_config() + return ( + not current_config.enabled + or current_config.options.webhook_url != url + or current_config.options.json_payload != json.loads(JSON_PAYLOAD) + or current_config.types != REGISTERED_NOTIFICATIONS + ) + + async def handle_webhook( + self, hass: HomeAssistant, webhook_id: str, request: Request + ) -> Response: + """Handle webhook.""" + data = await request.json() + LOGGER.debug("Received webhook payload: %s", data) + if data["notification_type"].startswith("MEDIA"): + await self.entry.runtime_data.async_refresh() + return HomeAssistantView.json({"message": "ok"}) + + async def unregister_webhook(self) -> None: + """Unregister webhook.""" + async_unregister(self.hass, self.entry.data[CONF_WEBHOOK_ID]) diff --git a/homeassistant/components/overseerr/config_flow.py b/homeassistant/components/overseerr/config_flow.py index f63949070d6..2ad0c8d6d61 100644 --- a/homeassistant/components/overseerr/config_flow.py +++ b/homeassistant/components/overseerr/config_flow.py @@ -7,8 +7,16 @@ from python_overseerr.exceptions import OverseerrError import voluptuous as vol from yarl import URL +from homeassistant.components.webhook import async_generate_id from homeassistant.config_entries import ConfigFlow, ConfigFlowResult -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, CONF_URL +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_WEBHOOK_ID, +) from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import DOMAIN @@ -49,6 +57,7 @@ class OverseerrConfigFlow(ConfigFlow, domain=DOMAIN): CONF_PORT: port, CONF_SSL: url.scheme == "https", CONF_API_KEY: user_input[CONF_API_KEY], + CONF_WEBHOOK_ID: async_generate_id(), }, ) return self.async_show_form( diff --git a/homeassistant/components/overseerr/const.py b/homeassistant/components/overseerr/const.py index b07b64338c8..64262c59d19 100644 --- a/homeassistant/components/overseerr/const.py +++ b/homeassistant/components/overseerr/const.py @@ -2,7 +2,44 @@ import logging +from python_overseerr.models import NotificationType + DOMAIN = "overseerr" LOGGER = logging.getLogger(__package__) REQUESTS = "requests" + +REGISTERED_NOTIFICATIONS = ( + NotificationType.REQUEST_PENDING_APPROVAL + | NotificationType.REQUEST_APPROVED + | NotificationType.REQUEST_DECLINED + | NotificationType.REQUEST_AVAILABLE + | NotificationType.REQUEST_PROCESSING_FAILED + | NotificationType.REQUEST_AUTOMATICALLY_APPROVED +) +JSON_PAYLOAD = ( + '"{\\"notification_type\\":\\"{{notification_type}}\\",\\"event\\":\\"' + '{{event}}\\",\\"subject\\":\\"{{subject}}\\",\\"message\\":\\"{{messa' + 'ge}}\\",\\"image\\":\\"{{image}}\\",\\"{{media}}\\":{\\"media_type\\"' + ':\\"{{media_type}}\\",\\"tmdbId\\":\\"{{media_tmdbid}}\\",\\"tvdbId\\' + '":\\"{{media_tvdbid}}\\",\\"status\\":\\"{{media_status}}\\",\\"statu' + 's4k\\":\\"{{media_status4k}}\\"},\\"{{request}}\\":{\\"request_id\\":' + '\\"{{request_id}}\\",\\"requestedBy_email\\":\\"{{requestedBy_email}}' + '\\",\\"requestedBy_username\\":\\"{{requestedBy_username}}\\",\\"requ' + 'estedBy_avatar\\":\\"{{requestedBy_avatar}}\\",\\"requestedBy_setting' + 's_discordId\\":\\"{{requestedBy_settings_discordId}}\\",\\"requestedB' + 'y_settings_telegramChatId\\":\\"{{requestedBy_settings_telegramChatId' + '}}\\"},\\"{{issue}}\\":{\\"issue_id\\":\\"{{issue_id}}\\",\\"issue_ty' + 'pe\\":\\"{{issue_type}}\\",\\"issue_status\\":\\"{{issue_status}}\\",' + '\\"reportedBy_email\\":\\"{{reportedBy_email}}\\",\\"reportedBy_usern' + 'ame\\":\\"{{reportedBy_username}}\\",\\"reportedBy_avatar\\":\\"{{rep' + 'ortedBy_avatar}}\\",\\"reportedBy_settings_discordId\\":\\"{{reported' + 'By_settings_discordId}}\\",\\"reportedBy_settings_telegramChatId\\":' + '\\"{{reportedBy_settings_telegramChatId}}\\"},\\"{{comment}}\\":{\\"c' + 'omment_message\\":\\"{{comment_message}}\\",\\"commentedBy_email\\":' + '\\"{{commentedBy_email}}\\",\\"commentedBy_username\\":\\"{{commented' + 'By_username}}\\",\\"commentedBy_avatar\\":\\"{{commentedBy_avatar}}' + '\\",\\"commentedBy_settings_discordId\\":\\"{{commentedBy_settings_di' + 'scordId}}\\",\\"commentedBy_settings_telegramChatId\\":\\"{{commented' + 'By_settings_telegramChatId}}\\"},\\"{{extra}}\\":[]\\n}"' +) diff --git a/homeassistant/components/overseerr/manifest.json b/homeassistant/components/overseerr/manifest.json index a759b48684a..1d3e369b395 100644 --- a/homeassistant/components/overseerr/manifest.json +++ b/homeassistant/components/overseerr/manifest.json @@ -3,9 +3,10 @@ "name": "Overseerr", "codeowners": ["@joostlek"], "config_flow": true, + "dependencies": ["http", "webhook"], "documentation": "https://www.home-assistant.io/integrations/overseerr", "integration_type": "service", - "iot_class": "local_polling", + "iot_class": "local_push", "quality_scale": "bronze", "requirements": ["python-overseerr==0.4.0"] } diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 41a20a4e723..af5b510b222 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -4587,7 +4587,7 @@ "name": "Overseerr", "integration_type": "service", "config_flow": true, - "iot_class": "local_polling" + "iot_class": "local_push" }, "ovo_energy": { "name": "OVO Energy", diff --git a/tests/components/overseerr/__init__.py b/tests/components/overseerr/__init__.py index db96435ecc2..b53b9d4f57a 100644 --- a/tests/components/overseerr/__init__.py +++ b/tests/components/overseerr/__init__.py @@ -1,7 +1,15 @@ """Tests for the Overseerr integration.""" +from typing import Any +from urllib.parse import urlparse + +from aiohttp.test_utils import TestClient + +from homeassistant.components.webhook import async_generate_url from homeassistant.core import HomeAssistant +from .const import WEBHOOK_ID + from tests.common import MockConfigEntry @@ -11,3 +19,21 @@ async def setup_integration(hass: HomeAssistant, config_entry: MockConfigEntry) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() + + +async def call_webhook( + hass: HomeAssistant, data: dict[str, Any], client: TestClient +) -> None: + """Call the webhook.""" + webhook_url = async_generate_url(hass, WEBHOOK_ID) + + resp = await client.post( + urlparse(webhook_url).path, + json=data, + ) + + # Wait for remaining tasks to complete. + await hass.async_block_till_done() + + data = await resp.json() + resp.close() diff --git a/tests/components/overseerr/conftest.py b/tests/components/overseerr/conftest.py index 4a3e6e48441..a98cfef321f 100644 --- a/tests/components/overseerr/conftest.py +++ b/tests/components/overseerr/conftest.py @@ -5,9 +5,18 @@ from unittest.mock import AsyncMock, patch import pytest from python_overseerr import RequestCount +from python_overseerr.models import WebhookNotificationConfig from homeassistant.components.overseerr.const import DOMAIN -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_WEBHOOK_ID, +) + +from .const import WEBHOOK_ID from tests.common import MockConfigEntry, load_fixture @@ -39,6 +48,12 @@ def mock_overseerr_client() -> Generator[AsyncMock]: client.get_request_count.return_value = RequestCount.from_json( load_fixture("request_count.json", DOMAIN) ) + client.get_webhook_notification_config.return_value = ( + WebhookNotificationConfig.from_json( + load_fixture("webhook_config.json", DOMAIN) + ) + ) + client.test_webhook_notification_config.return_value = True yield client @@ -53,6 +68,7 @@ def mock_config_entry() -> MockConfigEntry: CONF_PORT: 80, CONF_SSL: False, CONF_API_KEY: "test-key", + CONF_WEBHOOK_ID: WEBHOOK_ID, }, entry_id="01JG00V55WEVTJ0CJHM0GAD7PC", ) diff --git a/tests/components/overseerr/const.py b/tests/components/overseerr/const.py new file mode 100644 index 00000000000..8c5c86e3ee4 --- /dev/null +++ b/tests/components/overseerr/const.py @@ -0,0 +1,3 @@ +"""Constants for the Overseerr tests.""" + +WEBHOOK_ID = "test-webhook-id" diff --git a/tests/components/overseerr/fixtures/webhook_config.json b/tests/components/overseerr/fixtures/webhook_config.json new file mode 100644 index 00000000000..a4d07d6e9d3 --- /dev/null +++ b/tests/components/overseerr/fixtures/webhook_config.json @@ -0,0 +1,8 @@ +{ + "enabled": true, + "types": 222, + "options": { + "jsonPayload": "{\"notification_type\":\"{{notification_type}}\",\"event\":\"{{event}}\",\"subject\":\"{{subject}}\",\"message\":\"{{message}}\",\"image\":\"{{image}}\",\"{{media}}\":{\"media_type\":\"{{media_type}}\",\"tmdbId\":\"{{media_tmdbid}}\",\"tvdbId\":\"{{media_tvdbid}}\",\"status\":\"{{media_status}}\",\"status4k\":\"{{media_status4k}}\"},\"{{request}}\":{\"request_id\":\"{{request_id}}\",\"requestedBy_email\":\"{{requestedBy_email}}\",\"requestedBy_username\":\"{{requestedBy_username}}\",\"requestedBy_avatar\":\"{{requestedBy_avatar}}\",\"requestedBy_settings_discordId\":\"{{requestedBy_settings_discordId}}\",\"requestedBy_settings_telegramChatId\":\"{{requestedBy_settings_telegramChatId}}\"},\"{{issue}}\":{\"issue_id\":\"{{issue_id}}\",\"issue_type\":\"{{issue_type}}\",\"issue_status\":\"{{issue_status}}\",\"reportedBy_email\":\"{{reportedBy_email}}\",\"reportedBy_username\":\"{{reportedBy_username}}\",\"reportedBy_avatar\":\"{{reportedBy_avatar}}\",\"reportedBy_settings_discordId\":\"{{reportedBy_settings_discordId}}\",\"reportedBy_settings_telegramChatId\":\"{{reportedBy_settings_telegramChatId}}\"},\"{{comment}}\":{\"comment_message\":\"{{comment_message}}\",\"commentedBy_email\":\"{{commentedBy_email}}\",\"commentedBy_username\":\"{{commentedBy_username}}\",\"commentedBy_avatar\":\"{{commentedBy_avatar}}\",\"commentedBy_settings_discordId\":\"{{commentedBy_settings_discordId}}\",\"commentedBy_settings_telegramChatId\":\"{{commentedBy_settings_telegramChatId}}\"},\"{{extra}}\":[]\n}", + "webhookUrl": "http://10.10.10.10:8123/api/webhook/test-webhook-id" + } +} diff --git a/tests/components/overseerr/fixtures/webhook_request_automatically_approved.json b/tests/components/overseerr/fixtures/webhook_request_automatically_approved.json new file mode 100644 index 00000000000..cc8795c9821 --- /dev/null +++ b/tests/components/overseerr/fixtures/webhook_request_automatically_approved.json @@ -0,0 +1,25 @@ +{ + "notification_type": "MEDIA_AUTO_APPROVED", + "event": "Movie Request Automatically Approved", + "subject": "Something (2024)", + "message": "Here is an interesting Linux ISO that was automatically approved.", + "image": "https://image.tmdb.org/t/p/w600_and_h900_bestv2/something.jpg", + "media": { + "media_type": "movie", + "tmdbId": "123", + "tvdbId": "", + "status": "PENDING", + "status4k": "UNKNOWN" + }, + "request": { + "request_id": "16", + "requestedBy_email": "my@email.com", + "requestedBy_username": "henk", + "requestedBy_avatar": "https://plex.tv/users/abc/avatar?c=123", + "requestedBy_settings_discordId": "123", + "requestedBy_settings_telegramChatId": "" + }, + "issue": null, + "comment": null, + "extra": [] +} diff --git a/tests/components/overseerr/fixtures/webhook_test_notification.json b/tests/components/overseerr/fixtures/webhook_test_notification.json new file mode 100644 index 00000000000..4bae2ed742c --- /dev/null +++ b/tests/components/overseerr/fixtures/webhook_test_notification.json @@ -0,0 +1,12 @@ +{ + "notification_type": "TEST_NOTIFICATION", + "event": "", + "subject": "Test Notification", + "message": "Check check, 1, 2, 3. Are we coming in clear?", + "image": "", + "media": null, + "request": null, + "issue": null, + "comment": null, + "extra": [] +} diff --git a/tests/components/overseerr/test_config_flow.py b/tests/components/overseerr/test_config_flow.py index 7001ccd98a8..487c843ff1c 100644 --- a/tests/components/overseerr/test_config_flow.py +++ b/tests/components/overseerr/test_config_flow.py @@ -1,18 +1,38 @@ """Tests for the Overseerr config flow.""" -from unittest.mock import AsyncMock +from unittest.mock import AsyncMock, patch +import pytest from python_overseerr.exceptions import OverseerrConnectionError from homeassistant.components.overseerr.const import DOMAIN from homeassistant.config_entries import SOURCE_USER -from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT, CONF_SSL, CONF_URL +from homeassistant.const import ( + CONF_API_KEY, + CONF_HOST, + CONF_PORT, + CONF_SSL, + CONF_URL, + CONF_WEBHOOK_ID, +) from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from .const import WEBHOOK_ID + from tests.common import MockConfigEntry +@pytest.fixture(autouse=True) +def patch_webhook_id() -> None: + """Patch webhook ID generation.""" + with patch( + "homeassistant.components.overseerr.config_flow.async_generate_id", + return_value=WEBHOOK_ID, + ): + yield + + async def test_full_flow( hass: HomeAssistant, mock_overseerr_client: AsyncMock, @@ -37,6 +57,7 @@ async def test_full_flow( CONF_PORT: 80, CONF_SSL: False, CONF_API_KEY: "test-key", + CONF_WEBHOOK_ID: "test-webhook-id", } diff --git a/tests/components/overseerr/test_init.py b/tests/components/overseerr/test_init.py index f2755e4a61a..853859d9b39 100644 --- a/tests/components/overseerr/test_init.py +++ b/tests/components/overseerr/test_init.py @@ -1,9 +1,13 @@ """Tests for the Overseerr integration.""" +from typing import Any from unittest.mock import AsyncMock +import pytest +from python_overseerr.models import WebhookNotificationOptions from syrupy import SnapshotAssertion +from homeassistant.components.overseerr import JSON_PAYLOAD, REGISTERED_NOTIFICATIONS from homeassistant.components.overseerr.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.helpers import device_registry as dr @@ -27,3 +31,100 @@ async def test_device_info( ) assert device_entry is not None assert device_entry == snapshot + + +async def test_proper_webhook_configuration( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, +) -> None: + """Test the webhook configuration.""" + await setup_integration(hass, mock_config_entry) + + assert REGISTERED_NOTIFICATIONS == 222 + + mock_overseerr_client.test_webhook_notification_config.assert_not_called() + mock_overseerr_client.set_webhook_notification_config.assert_not_called() + + +@pytest.mark.parametrize( + "update_mock", + [ + {"return_value.enabled": False}, + {"return_value.types": 4}, + {"return_value.types": 4062}, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://example.com", json_payload=JSON_PAYLOAD + ) + }, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://10.10.10.10:8123/api/webhook/test-webhook-id", + json_payload='"{\\"message\\": \\"{{title}}\\"}"', + ) + }, + ], + ids=[ + "Disabled", + "Smaller scope", + "Bigger scope", + "Webhook URL", + "JSON Payload", + ], +) +async def test_webhook_configuration_need_update( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, + update_mock: dict[str, Any], +) -> None: + """Test the webhook configuration.""" + mock_overseerr_client.get_webhook_notification_config.configure_mock(**update_mock) + + await setup_integration(hass, mock_config_entry) + + mock_overseerr_client.test_webhook_notification_config.assert_called_once() + mock_overseerr_client.set_webhook_notification_config.assert_called_once() + + +@pytest.mark.parametrize( + "update_mock", + [ + {"return_value.enabled": False}, + {"return_value.types": 4}, + {"return_value.types": 4062}, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://example.com", json_payload=JSON_PAYLOAD + ) + }, + { + "return_value.options": WebhookNotificationOptions( + webhook_url="http://10.10.10.10:8123/api/webhook/test-webhook-id", + json_payload='"{\\"message\\": \\"{{title}}\\"}"', + ) + }, + ], + ids=[ + "Disabled", + "Smaller scope", + "Bigger scope", + "Webhook URL", + "JSON Payload", + ], +) +async def test_webhook_failing_test( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_overseerr_client: AsyncMock, + update_mock: dict[str, Any], +) -> None: + """Test the webhook configuration.""" + mock_overseerr_client.test_webhook_notification_config.return_value = False + mock_overseerr_client.get_webhook_notification_config.configure_mock(**update_mock) + + await setup_integration(hass, mock_config_entry) + + mock_overseerr_client.test_webhook_notification_config.assert_called_once() + mock_overseerr_client.set_webhook_notification_config.assert_not_called() diff --git a/tests/components/overseerr/test_sensor.py b/tests/components/overseerr/test_sensor.py index 9c26ae54df8..6689b1ebcc3 100644 --- a/tests/components/overseerr/test_sensor.py +++ b/tests/components/overseerr/test_sensor.py @@ -4,13 +4,15 @@ from unittest.mock import AsyncMock, patch from syrupy import SnapshotAssertion +from homeassistant.components.overseerr import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import setup_integration +from . import call_webhook, setup_integration -from tests.common import MockConfigEntry, snapshot_platform +from tests.common import MockConfigEntry, load_json_object_fixture, snapshot_platform +from tests.typing import ClientSessionGenerator async def test_all_entities( @@ -25,3 +27,27 @@ async def test_all_entities( await setup_integration(hass, mock_config_entry) await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id) + + +async def test_webhook_trigger_update( + hass: HomeAssistant, + mock_overseerr_client: AsyncMock, + mock_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, +) -> None: + """Test all entities.""" + await setup_integration(hass, mock_config_entry) + + assert hass.states.get("sensor.overseerr_available_requests").state == "8" + + mock_overseerr_client.get_request_count.return_value.available = 7 + client = await hass_client_no_auth() + + await call_webhook( + hass, + load_json_object_fixture("webhook_request_automatically_approved.json", DOMAIN), + client, + ) + await hass.async_block_till_done() + + assert hass.states.get("sensor.overseerr_available_requests").state == "7"