From 219b9cbcaa86def6b961e00e853745cc74874ae3 Mon Sep 17 00:00:00 2001 From: Boris Obmoroshev Date: Thu, 28 May 2026 19:18:24 +0100 Subject: [PATCH] Add regression test for ONVIF setup against a real ONVIFDevice (#172194) Co-authored-by: Claude --- tests/components/onvif/__init__.py | 68 +++++++++++++++++++++- tests/components/onvif/test_init.py | 49 +++++++++++++++- tests/components/onvif/test_ptz.py | 89 ++--------------------------- 3 files changed, 121 insertions(+), 85 deletions(-) diff --git a/tests/components/onvif/__init__.py b/tests/components/onvif/__init__.py index 9b2c0bb64fe6..7e0d8cc265c8 100644 --- a/tests/components/onvif/__init__.py +++ b/tests/components/onvif/__init__.py @@ -2,6 +2,7 @@ import collections from collections import defaultdict +from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch from onvif.exceptions import ONVIFError @@ -53,8 +54,17 @@ def setup_mock_onvif_camera( no_profiles=False, auth_failure=False, wrong_port=False, + with_full_setup=False, ): - """Prepare mock onvif.ONVIFCamera.""" + """Prepare mock onvif.ONVIFCamera. + + When ``with_full_setup`` is set, the mock is additionally configured with + every service ``ONVIFDevice.async_setup`` invokes, so that a config entry + can be fully set up end-to-end. In this mode the profile and device + information responses are replaced, so the flags that control them + (``with_h264``, ``two_profiles``, ``no_profiles``, ``auth_fail`` and + ``profiles_transient_failure``) have no effect. + """ devicemgmt = MagicMock() device_info = MagicMock() @@ -111,6 +121,62 @@ def setup_mock_onvif_camera( mock_onvif_camera.xaddrs = {} mock_onvif_camera.services = {} + if with_full_setup: + devicemgmt.GetSystemDateAndTime = AsyncMock(return_value=None) + devicemgmt.GetDeviceInformation = AsyncMock( + return_value=SimpleNamespace( + Manufacturer=MANUFACTURER, + Model=MODEL, + FirmwareVersion=FIRMWARE_VERSION, + SerialNumber=SERIAL_NUMBER if with_serial else None, + ) + ) + + media_service.GetServiceCapabilities = AsyncMock( + return_value=SimpleNamespace(SnapshotUri=False) + ) + media_service.GetProfiles = AsyncMock( + return_value=[ + SimpleNamespace( + token="profile_token", + Name="MainStream", + VideoEncoderConfiguration=SimpleNamespace( + Resolution=SimpleNamespace(Width=1920, Height=1080), + Encoding="H264", + ), + VideoSourceConfiguration=MagicMock(), + PTZConfiguration=MagicMock(), + ) + ] + ) + + ptz_service = MagicMock() + ptz_service.GetPresets = AsyncMock(return_value=[]) + mock_onvif_camera.create_ptz_service = AsyncMock(return_value=ptz_service) + mock_onvif_camera.create_imaging_service = AsyncMock(return_value=MagicMock()) + mock_onvif_camera.get_snapshot = AsyncMock(return_value=False) + mock_onvif_camera.get_capabilities = AsyncMock( + return_value={ + "Media": {"XAddr": "http://media"}, + "PTZ": {"XAddr": "http://ptz"}, + "Imaging": {"XAddr": "http://imaging"}, + "Events": { + "XAddr": None, + "WSPullPointSupport": False, + "WSSubscriptionPolicySupport": False, + }, + } + ) + # Let the real event managers run but fail gracefully at the onvif + # library boundary, so async_start_events returns False and setup + # still reaches LOADED (models a camera without working events). + mock_onvif_camera.create_pullpoint_manager = AsyncMock( + side_effect=Fault("no pullpoint support") + ) + mock_onvif_camera.create_notification_manager = AsyncMock( + side_effect=Fault("no notification support") + ) + def mock_constructor( host, port, diff --git a/tests/components/onvif/test_init.py b/tests/components/onvif/test_init.py index 3a4a5dcb33d5..161cc2b36706 100644 --- a/tests/components/onvif/test_init.py +++ b/tests/components/onvif/test_init.py @@ -2,10 +2,27 @@ from unittest.mock import MagicMock, patch +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import ( + CONF_HOST, + CONF_NAME, + CONF_PASSWORD, + CONF_PORT, + CONF_USERNAME, +) from homeassistant.core import HomeAssistant from homeassistant.helpers import entity_registry as er -from . import MAC, setup_mock_device +from . import ( + HOST, + MAC, + NAME, + PASSWORD, + PORT, + USERNAME, + setup_mock_device, + setup_mock_onvif_camera, +) from tests.common import MockConfigEntry @@ -97,3 +114,33 @@ async def test_migrate_camera_entities_unique_ids(hass: HomeAssistant) -> None: # Make sure the unexisting index entity is unchanged assert entity_unexisting_index is not None assert entity_unexisting_index.unique_id == f"{MAC}_9" + + +async def test_setup_entry(hass: HomeAssistant) -> None: + """Test setting up the config entry.""" + entry = MockConfigEntry( + domain="onvif", + title=NAME, + unique_id=MAC, + data={ + CONF_NAME: NAME, + CONF_HOST: HOST, + CONF_PORT: PORT, + CONF_USERNAME: USERNAME, + CONF_PASSWORD: PASSWORD, + }, + ) + entry.add_to_hass(hass) + + with patch( + "homeassistant.components.onvif.device.ONVIFCamera" + ) as mock_onvif_camera_cls: + setup_mock_onvif_camera(mock_onvif_camera_cls, with_full_setup=True) + + assert await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + + assert entry.state is ConfigEntryState.LOADED + mock_onvif_camera_cls.assert_called_once() + host, port, username, password = mock_onvif_camera_cls.call_args.args[:4] + assert (host, port, username, password) == (HOST, PORT, USERNAME, PASSWORD) diff --git a/tests/components/onvif/test_ptz.py b/tests/components/onvif/test_ptz.py index b5b8b147e161..60bb091cd703 100644 --- a/tests/components/onvif/test_ptz.py +++ b/tests/components/onvif/test_ptz.py @@ -1,6 +1,5 @@ """Test ONVIF PTZ capabilities.""" -from types import SimpleNamespace from unittest.mock import AsyncMock, MagicMock, patch from homeassistant.const import ( @@ -12,19 +11,7 @@ from homeassistant.const import ( ) from homeassistant.core import HomeAssistant -from . import ( - FIRMWARE_VERSION, - HOST, - MAC, - MANUFACTURER, - MODEL, - NAME, - PASSWORD, - PORT, - SERIAL_NUMBER, - USERNAME, - setup_mock_onvif_camera, -) +from . import HOST, MAC, NAME, PASSWORD, PORT, USERNAME, setup_mock_onvif_camera from tests.common import MockConfigEntry @@ -32,71 +19,15 @@ from tests.common import MockConfigEntry def _setup_ptz_camera_mocks(mock_onvif_camera_cls: MagicMock) -> MagicMock: """Configure the patched ONVIFCamera class for PTZ tests. - Builds on the shared `setup_mock_onvif_camera` helper and only adds the - extra mocks needed for `ONVIFDevice.async_setup` to complete end-to-end - with PTZ capability. Returns the ptz_service mock so callers can assert - against ContinuousMove/Stop calls. + Uses the shared ``setup_mock_onvif_camera`` full-setup helper and only adds + the PTZ service mocks the tests assert against, returning the ptz_service + mock so callers can check ContinuousMove/Stop calls. """ - setup_mock_onvif_camera(mock_onvif_camera_cls) + setup_mock_onvif_camera(mock_onvif_camera_cls, with_full_setup=True) - # Override GetProfiles with a profile that has the attributes the PTZ - # code path reads from (PTZConfiguration, VideoSourceConfiguration, - # Resolution width/height, and a stable Name for entity_id generation). - media_service = MagicMock() - media_service.GetServiceCapabilities = AsyncMock( - return_value=SimpleNamespace(SnapshotUri=False) - ) - media_service.GetProfiles = AsyncMock( - return_value=[ - SimpleNamespace( - token="profile_token", - Name="MainStream", - VideoEncoderConfiguration=SimpleNamespace( - Resolution=SimpleNamespace(Width=1920, Height=1080), - Encoding="H264", - ), - VideoSourceConfiguration=MagicMock(), - PTZConfiguration=MagicMock(), - ) - ] - ) - mock_onvif_camera_cls.create_media_service = AsyncMock(return_value=media_service) - - ptz_service = MagicMock() + ptz_service = mock_onvif_camera_cls.create_ptz_service.return_value ptz_service.ContinuousMove = AsyncMock() ptz_service.Stop = AsyncMock() - ptz_service.GetPresets = AsyncMock(return_value=[]) - mock_onvif_camera_cls.create_ptz_service = AsyncMock(return_value=ptz_service) - mock_onvif_camera_cls.create_imaging_service = AsyncMock(return_value=MagicMock()) - mock_onvif_camera_cls.create_pullpoint_manager = AsyncMock(return_value=MagicMock()) - mock_onvif_camera_cls.get_snapshot = AsyncMock(return_value=False) - mock_onvif_camera_cls.get_capabilities = AsyncMock( - return_value={ - "Media": {"XAddr": "http://media"}, - "PTZ": {"XAddr": "http://ptz"}, - "Imaging": {"XAddr": "http://imaging"}, - "Events": { - "XAddr": None, - "WSPullPointSupport": False, - "WSSubscriptionPolicySupport": False, - }, - } - ) - - # Short-circuit async_check_date_and_time by returning None, and return - # serializable device information so the device registry entry can be - # persisted (the shared helper only sets SerialNumber). - devicemgmt = mock_onvif_camera_cls.create_devicemgmt_service.return_value - devicemgmt.GetSystemDateAndTime = AsyncMock(return_value=None) - devicemgmt.GetDeviceInformation = AsyncMock( - return_value=SimpleNamespace( - Manufacturer=MANUFACTURER, - Model=MODEL, - FirmwareVersion=FIRMWARE_VERSION, - SerialNumber=SERIAL_NUMBER, - ) - ) - return ptz_service @@ -149,10 +80,6 @@ async def test_ptz_continuous_move_calls_stop_when_duration_nonzero( "homeassistant.components.onvif.device.asyncio.sleep", new=AsyncMock(), ) as mock_sleep, - patch( - "homeassistant.components.onvif.device.ONVIFDevice.async_start_events", - new=AsyncMock(return_value=False), - ), ): ptz_service = _setup_ptz_camera_mocks(mock_onvif_camera_cls) @@ -183,10 +110,6 @@ async def test_ptz_continuous_move_does_not_call_stop_when_duration_zero( "homeassistant.components.onvif.device.asyncio.sleep", new=AsyncMock(), ) as mock_sleep, - patch( - "homeassistant.components.onvif.device.ONVIFDevice.async_start_events", - new=AsyncMock(return_value=False), - ), ): ptz_service = _setup_ptz_camera_mocks(mock_onvif_camera_cls)