Add regression test for ONVIF setup against a real ONVIFDevice (#172194)

Co-authored-by: Claude <noreply@anthropic.com>
This commit is contained in:
Boris Obmoroshev
2026-05-28 19:18:24 +01:00
committed by GitHub
co-authored by Claude
parent 309b26f809
commit 219b9cbcaa
3 changed files with 121 additions and 85 deletions
+67 -1
View File
@@ -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,
+48 -1
View File
@@ -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)
+6 -83
View File
@@ -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)