mirror of
https://github.com/home-assistant/core.git
synced 2026-09-08 06:31:35 +01:00
788 lines
26 KiB
Python
788 lines
26 KiB
Python
"""Fixtures and test data for UniFi Protect methods."""
|
|
|
|
from collections.abc import Callable, Coroutine, Generator, Iterator
|
|
from datetime import datetime, timedelta
|
|
from functools import partial
|
|
from ipaddress import IPv4Address
|
|
from pathlib import Path
|
|
from tempfile import gettempdir
|
|
from typing import Any
|
|
from unittest.mock import AsyncMock, Mock, PropertyMock, patch
|
|
|
|
import pytest
|
|
from uiprotect import ProtectApiClient
|
|
from uiprotect.api import RTSPSStreams
|
|
from uiprotect.data import (
|
|
NVR,
|
|
AiPort,
|
|
Bootstrap,
|
|
Camera,
|
|
Chime,
|
|
CloudAccount,
|
|
Light,
|
|
Liveview,
|
|
ModelType,
|
|
NvrArmMode,
|
|
NvrArmModeStatus,
|
|
ProtectModelWithId,
|
|
PublicBootstrap,
|
|
Sensor,
|
|
SmartDetectObjectType,
|
|
StateType,
|
|
Version,
|
|
VideoMode,
|
|
Viewer,
|
|
WSSubscriptionMessage,
|
|
)
|
|
from uiprotect.data.public_devices import PublicNVR
|
|
from uiprotect.exceptions import BadRequest, PublicOnlyModeError
|
|
from uiprotect.websocket import WebsocketState
|
|
|
|
from homeassistant.components.unifiprotect.const import (
|
|
CONF_CONNECTION_MODE,
|
|
CONNECTION_MODE_API_KEY_ONLY,
|
|
DOMAIN,
|
|
)
|
|
from homeassistant.components.unifiprotect.utils import _async_unifi_mac_from_hass
|
|
from homeassistant.const import (
|
|
CONF_API_KEY,
|
|
CONF_HOST,
|
|
CONF_PASSWORD,
|
|
CONF_PORT,
|
|
CONF_USERNAME,
|
|
CONF_VERIFY_SSL,
|
|
)
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.util import dt as dt_util
|
|
|
|
from . import _patch_discovery
|
|
from .utils import MockUFPFixture, make_public_camera, public_rtsps_for
|
|
|
|
from tests.common import MockConfigEntry, load_json_object_fixture
|
|
|
|
MAC_ADDR = "aa:bb:cc:dd:ee:ff"
|
|
UNIFI_MAC = _async_unifi_mac_from_hass(MAC_ADDR)
|
|
|
|
# The public-only NVR mock is named "Test NVR" (see ``ufp_public_only_client``)
|
|
PUBLIC_ONLY_ALARM_ENTITY_ID = "alarm_control_panel.test_nvr_alarm_manager"
|
|
|
|
# Common test data constants
|
|
DEFAULT_HOST = "1.1.1.1"
|
|
DEFAULT_PORT = 443
|
|
DEFAULT_VERIFY_SSL = False
|
|
DEFAULT_USERNAME = "test-username"
|
|
DEFAULT_PASSWORD = "test-password"
|
|
DEFAULT_API_KEY = "test-api-key"
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_discovery():
|
|
"""Prevent real network scanning in all unifiprotect tests."""
|
|
with _patch_discovery(no_device=True):
|
|
yield
|
|
|
|
|
|
@pytest.fixture(name="nvr")
|
|
def mock_nvr():
|
|
"""Mock UniFi Protect Camera device."""
|
|
|
|
data = load_json_object_fixture("sample_nvr.json", DOMAIN)
|
|
nvr = NVR.from_unifi_dict(**data)
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
NVR.model_config["validate_assignment"] = False
|
|
|
|
yield nvr
|
|
|
|
NVR.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="ufp_options")
|
|
def mock_ufp_options(request: pytest.FixtureRequest) -> dict[str, Any]:
|
|
"""Options for the mock config entry (override per-test via indirect param)."""
|
|
options: dict[str, Any] = {}
|
|
if hasattr(request, "param"):
|
|
options.update(request.param)
|
|
return options
|
|
|
|
|
|
@pytest.fixture(name="ufp_config_entry")
|
|
def mock_ufp_config_entry(ufp_options: dict[str, Any]):
|
|
"""Mock the unifiprotect config entry."""
|
|
|
|
return MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_HOST: DEFAULT_HOST,
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: DEFAULT_PASSWORD,
|
|
CONF_API_KEY: DEFAULT_API_KEY,
|
|
"id": "UnifiProtect",
|
|
CONF_PORT: DEFAULT_PORT,
|
|
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
|
},
|
|
options=ufp_options,
|
|
version=2,
|
|
unique_id="A1E00C826924",
|
|
)
|
|
|
|
|
|
@pytest.fixture(name="old_nvr")
|
|
def old_nvr():
|
|
"""Mock UniFi Protect Camera device."""
|
|
|
|
data = load_json_object_fixture("sample_nvr.json", DOMAIN)
|
|
data["version"] = "1.19.0"
|
|
return NVR.from_unifi_dict(**data)
|
|
|
|
|
|
@pytest.fixture(name="bootstrap")
|
|
def bootstrap_fixture(nvr: NVR):
|
|
"""Mock Bootstrap fixture."""
|
|
data = load_json_object_fixture("sample_bootstrap.json", DOMAIN)
|
|
data["nvr"] = nvr
|
|
data["cameras"] = []
|
|
data["lights"] = []
|
|
data["sensors"] = []
|
|
data["viewers"] = []
|
|
data["liveviews"] = []
|
|
data["events"] = []
|
|
data["chimes"] = []
|
|
data["aiports"] = []
|
|
|
|
return Bootstrap.from_unifi_dict(**data)
|
|
|
|
|
|
@pytest.fixture(name="ufp_client")
|
|
def mock_ufp_client(bootstrap: Bootstrap):
|
|
"""Mock ProtectApiClient for testing."""
|
|
client = Mock()
|
|
client.bootstrap = bootstrap
|
|
client._bootstrap = bootstrap
|
|
client.api_path = "/api"
|
|
client.cache_dir = Path(gettempdir()) / "ufp_cache"
|
|
# functionality from API client tests actually need
|
|
client._stream_response = partial(ProtectApiClient._stream_response, client)
|
|
client.get_camera_video = partial(ProtectApiClient.get_camera_video, client)
|
|
|
|
nvr = client.bootstrap.nvr
|
|
nvr._api = client
|
|
client.bootstrap._api = client
|
|
|
|
client.base_url = "https://127.0.0.1"
|
|
client.connection_host = IPv4Address("127.0.0.1")
|
|
|
|
async def get_nvr(*args: Any, **kwargs: Any) -> NVR:
|
|
return client.bootstrap.nvr
|
|
|
|
client.get_nvr = get_nvr
|
|
client.get_bootstrap = AsyncMock(return_value=bootstrap)
|
|
client.update = AsyncMock(return_value=bootstrap)
|
|
client.update_public = AsyncMock()
|
|
client.async_disconnect_ws = AsyncMock()
|
|
client.has_public_bootstrap = True
|
|
client.is_public_only = False
|
|
|
|
# Only consulted on the public-only setup path (tests that flip
|
|
# ``is_public_only`` on this client); a valid version and NVR mac let
|
|
# that path complete instead of requiring per-test setup.
|
|
meta = Mock()
|
|
meta.version = Version("7.2.105")
|
|
client.get_meta_info = AsyncMock(return_value=meta)
|
|
|
|
# The library owns RTSPS streams on ``PublicCamera.rtsps_streams`` and primes
|
|
# them in ``update_public()``; the integration reads them synchronously. Start
|
|
# with empty collections; the ``update_public`` side effect (see ``mock_entry``)
|
|
# primes the cameras from the private bootstrap.
|
|
client.public_bootstrap = Mock(spec=PublicBootstrap)
|
|
client.public_bootstrap.cameras = {}
|
|
client.public_bootstrap.lights = {}
|
|
client.public_bootstrap.relays = {}
|
|
client.public_bootstrap.sirens = {}
|
|
client.public_bootstrap.arm_profiles = {}
|
|
client.public_bootstrap.arm_mode = None
|
|
client.public_bootstrap.nvr = Mock()
|
|
client.public_bootstrap.nvr.mac = nvr.mac
|
|
client.public_bootstrap.nvr.name = nvr.name
|
|
client.public_bootstrap.nvr.display_name = nvr.name
|
|
client.public_bootstrap.nvr.device_type = None
|
|
client.public_bootstrap.nvr.type = None
|
|
|
|
# Cameras and lights resolve to their primed public model (see
|
|
# ``update_public`` in ``mock_entry`` / ``setup_public_light``); other
|
|
# device types opt in via the ``setup_public_*`` helpers, so they default
|
|
# to no paired public object.
|
|
def _public_bootstrap_get(
|
|
model: ModelType, obj_id: str
|
|
) -> ProtectModelWithId | None:
|
|
if model is ModelType.CAMERA:
|
|
return client.public_bootstrap.cameras.get(obj_id)
|
|
if model is ModelType.LIGHT:
|
|
return client.public_bootstrap.lights.get(obj_id)
|
|
return None
|
|
|
|
client.public_bootstrap.get = Mock(side_effect=_public_bootstrap_get)
|
|
|
|
def _public_all_devices() -> Iterator[Mock]:
|
|
pb = client.public_bootstrap
|
|
yield from pb.cameras.values()
|
|
yield from pb.lights.values()
|
|
yield from pb.relays.values()
|
|
yield from pb.sirens.values()
|
|
|
|
client.public_bootstrap.all_devices = _public_all_devices
|
|
|
|
async def get_camera_rtsps_streams(
|
|
camera_id: str, *args: Any, **kwargs: Any
|
|
) -> RTSPSStreams | None:
|
|
"""Fetch a camera's RTSPS streams (used by the repair flow)."""
|
|
camera = client.bootstrap.cameras.get(camera_id)
|
|
return public_rtsps_for(camera) if camera is not None else None
|
|
|
|
client.get_camera_rtsps_streams = AsyncMock(side_effect=get_camera_rtsps_streams)
|
|
client.create_camera_rtsps_streams = AsyncMock(return_value=None)
|
|
return client
|
|
|
|
|
|
@pytest.fixture(name="ufp")
|
|
def mock_entry(
|
|
hass: HomeAssistant, ufp_config_entry: MockConfigEntry, ufp_client: ProtectApiClient
|
|
):
|
|
"""Mock ProtectApiClient for testing."""
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.unifiprotect.utils.ProtectApiClient"
|
|
) as mock_api,
|
|
):
|
|
ufp_config_entry.add_to_hass(hass)
|
|
|
|
mock_api.return_value = ufp_client
|
|
|
|
ufp = MockUFPFixture(ufp_config_entry, ufp_client)
|
|
|
|
def subscribe(ws_callback: Callable[[WSSubscriptionMessage], None]) -> Any:
|
|
ufp.ws_subscription = ws_callback
|
|
return Mock()
|
|
|
|
def subscribe_websocket_state(
|
|
ws_state_subscription: Callable[[WebsocketState], None],
|
|
) -> Any:
|
|
ufp.ws_state_subscription = ws_state_subscription
|
|
return Mock()
|
|
|
|
def subscribe_devices_websocket(
|
|
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
) -> Any:
|
|
ufp.devices_ws_subscription = ws_callback
|
|
return Mock()
|
|
|
|
def subscribe_events(events_callback: Callable[..., None]) -> Any:
|
|
# Mirror uiprotect: subscribe_events() requires update_public() to
|
|
# have primed the public bootstrap first, otherwise it raises.
|
|
if not ufp_client.has_public_bootstrap:
|
|
raise RuntimeError(
|
|
"subscribe_events() requires update_public() to have been"
|
|
" called at least once"
|
|
)
|
|
ufp.events_subscription = events_callback
|
|
return Mock()
|
|
|
|
def subscribe_devices_websocket_state(
|
|
ws_state_subscription: Callable[[WebsocketState], None],
|
|
) -> Any:
|
|
ufp.devices_ws_state_subscription = ws_state_subscription
|
|
return Mock()
|
|
|
|
def subscribe_events_websocket_state(
|
|
ws_state_subscription: Callable[[WebsocketState], None],
|
|
) -> Any:
|
|
ufp.events_ws_state_subscription = ws_state_subscription
|
|
return Mock()
|
|
|
|
ufp_client.subscribe_websocket = subscribe
|
|
ufp_client.subscribe_websocket_state = subscribe_websocket_state
|
|
ufp_client.subscribe_devices_websocket = subscribe_devices_websocket
|
|
ufp_client.subscribe_events = subscribe_events
|
|
ufp_client.subscribe_devices_websocket_state = subscribe_devices_websocket_state
|
|
ufp_client.subscribe_events_websocket_state = subscribe_events_websocket_state
|
|
|
|
async def update_public() -> Any:
|
|
# Mirror the library prime: build each camera's public model from the
|
|
# private bootstrap and attach its RTSPS streams (connected cameras
|
|
# only, so a disconnected camera stays streamless), keyed by id.
|
|
pb = ufp_client.public_bootstrap
|
|
cameras: dict[str, Any] = {}
|
|
for camera in ufp_client.bootstrap.cameras.values():
|
|
public = make_public_camera(camera)
|
|
public.rtsps_streams = (
|
|
public_rtsps_for(camera)
|
|
if camera.state is StateType.CONNECTED
|
|
else None
|
|
)
|
|
cameras[camera.id] = public
|
|
pb.cameras = cameras
|
|
return pb
|
|
|
|
ufp_client.update_public = AsyncMock(side_effect=update_public)
|
|
ufp_client.has_public_bootstrap = True
|
|
yield ufp
|
|
|
|
|
|
@pytest.fixture
|
|
def liveview():
|
|
"""Mock UniFi Protect Liveview."""
|
|
|
|
data = load_json_object_fixture("sample_liveview.json", DOMAIN)
|
|
return Liveview.from_unifi_dict(**data)
|
|
|
|
|
|
@pytest.fixture(name="camera")
|
|
def camera_fixture(fixed_now: datetime):
|
|
"""Mock UniFi Protect Camera device."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
Camera.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_camera.json", DOMAIN)
|
|
camera = Camera.from_unifi_dict(**data)
|
|
camera.last_motion = fixed_now - timedelta(hours=1)
|
|
|
|
yield camera
|
|
|
|
Camera.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="camera_all")
|
|
def camera_all_fixture(camera: Camera):
|
|
"""Mock UniFi Protect Camera device."""
|
|
|
|
all_camera = camera.model_copy()
|
|
all_camera.channels = [all_camera.channels[0].model_copy()]
|
|
|
|
medium_channel = all_camera.channels[0].model_copy()
|
|
medium_channel.name = "Medium"
|
|
medium_channel.id = 1
|
|
medium_channel.rtsp_alias = "test_medium_alias"
|
|
all_camera.channels.append(medium_channel)
|
|
|
|
low_channel = all_camera.channels[0].model_copy()
|
|
low_channel.name = "Low"
|
|
low_channel.id = 2
|
|
low_channel.rtsp_alias = "test_medium_alias"
|
|
all_camera.channels.append(low_channel)
|
|
|
|
return all_camera
|
|
|
|
|
|
@pytest.fixture(name="camera_all_features")
|
|
def camera_all_features_fixture(fixed_now: datetime):
|
|
"""Mock UniFi Protect Camera device with all features enabled."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
Camera.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_camera_all_features.json", DOMAIN)
|
|
camera = Camera.from_unifi_dict(**data)
|
|
camera.last_motion = fixed_now - timedelta(hours=1)
|
|
|
|
yield camera
|
|
|
|
Camera.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="doorbell")
|
|
def doorbell_fixture(camera: Camera, fixed_now: datetime):
|
|
"""Mock UniFi Protect Camera device (with chime)."""
|
|
|
|
doorbell = camera.model_copy()
|
|
doorbell.channels = [c.model_copy() for c in doorbell.channels]
|
|
|
|
package_channel = doorbell.channels[0].model_copy()
|
|
package_channel.name = "Package Camera"
|
|
package_channel.id = 3
|
|
package_channel.fps = 2
|
|
package_channel.rtsp_alias = "test_package_alias"
|
|
|
|
doorbell.channels.append(package_channel)
|
|
doorbell.feature_flags.video_modes = [VideoMode.DEFAULT, VideoMode.HIGH_FPS]
|
|
doorbell.feature_flags.smart_detect_types = [
|
|
SmartDetectObjectType.PERSON,
|
|
SmartDetectObjectType.VEHICLE,
|
|
SmartDetectObjectType.ANIMAL,
|
|
SmartDetectObjectType.PACKAGE,
|
|
]
|
|
doorbell.has_speaker = True
|
|
doorbell.feature_flags.has_hdr = True
|
|
doorbell.feature_flags.has_lcd_screen = True
|
|
doorbell.feature_flags.has_speaker = True
|
|
doorbell.feature_flags.has_privacy_mask = True
|
|
doorbell.feature_flags.is_doorbell = True
|
|
doorbell.feature_flags.has_fingerprint_sensor = True
|
|
doorbell.feature_flags.support_nfc = True
|
|
doorbell.feature_flags.has_chime = True
|
|
doorbell.feature_flags.has_smart_detect = True
|
|
doorbell.feature_flags.has_package_camera = True
|
|
doorbell.feature_flags.has_led_status = True
|
|
doorbell.last_ring = fixed_now - timedelta(hours=1)
|
|
return doorbell
|
|
|
|
|
|
@pytest.fixture(name="ptz_camera")
|
|
def ptz_camera_fixture(camera: Camera):
|
|
"""Mock UniFi Protect PTZ Camera device."""
|
|
ptz_cam = camera.model_copy()
|
|
ptz_cam.channels = [c.model_copy() for c in ptz_cam.channels]
|
|
ptz_cam.name = "PTZ Camera"
|
|
ptz_cam.feature_flags.is_ptz = True
|
|
ptz_cam.active_patrol_slot = None
|
|
|
|
# Disable pydantic validation on this instance so we can mock methods
|
|
object.__setattr__(ptz_cam, "get_ptz_presets", AsyncMock(return_value=[]))
|
|
object.__setattr__(ptz_cam, "get_ptz_patrols", AsyncMock(return_value=[]))
|
|
object.__setattr__(ptz_cam, "ptz_goto_preset_public", AsyncMock())
|
|
object.__setattr__(ptz_cam, "ptz_patrol_start_public", AsyncMock())
|
|
object.__setattr__(ptz_cam, "ptz_patrol_stop_public", AsyncMock())
|
|
|
|
return ptz_cam
|
|
|
|
|
|
@pytest.fixture
|
|
def unadopted_camera(camera: Camera):
|
|
"""Mock UniFi Protect Camera device (unadopted)."""
|
|
|
|
no_camera = camera.model_copy()
|
|
no_camera.channels = [c.model_copy() for c in no_camera.channels]
|
|
no_camera.name = "Unadopted Camera"
|
|
no_camera.is_adopted = False
|
|
return no_camera
|
|
|
|
|
|
@pytest.fixture(name="light")
|
|
def light_fixture():
|
|
"""Mock UniFi Protect Light device."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
Light.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_light.json", DOMAIN)
|
|
yield Light.from_unifi_dict(**data)
|
|
|
|
Light.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture
|
|
def unadopted_light(light: Light):
|
|
"""Mock UniFi Protect Light device (unadopted)."""
|
|
|
|
no_light = light.model_copy()
|
|
no_light.name = "Unadopted Light"
|
|
no_light.is_adopted = False
|
|
return no_light
|
|
|
|
|
|
@pytest.fixture
|
|
def viewer():
|
|
"""Mock UniFi Protect Viewport device."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
Viewer.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_viewport.json", DOMAIN)
|
|
yield Viewer.from_unifi_dict(**data)
|
|
|
|
Viewer.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="sensor")
|
|
def sensor_fixture(fixed_now: datetime):
|
|
"""Mock UniFi Protect Sensor device."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
Sensor.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_sensor.json", DOMAIN)
|
|
sensor: Sensor = Sensor.from_unifi_dict(**data)
|
|
sensor.motion_detected_at = fixed_now - timedelta(hours=1)
|
|
sensor.open_status_changed_at = fixed_now - timedelta(hours=1)
|
|
sensor.alarm_triggered_at = fixed_now - timedelta(hours=1)
|
|
yield sensor
|
|
|
|
Sensor.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="sensor_all")
|
|
def sensor_all_fixture(sensor: Sensor):
|
|
"""Mock UniFi Protect Sensor device."""
|
|
|
|
all_sensor = sensor.model_copy()
|
|
all_sensor.light_settings.is_enabled = True
|
|
all_sensor.humidity_settings.is_enabled = True
|
|
all_sensor.temperature_settings.is_enabled = True
|
|
all_sensor.alarm_settings.is_enabled = True
|
|
all_sensor.led_settings.is_enabled = True
|
|
all_sensor.motion_settings.is_enabled = True
|
|
|
|
return all_sensor
|
|
|
|
|
|
@pytest.fixture
|
|
def chime():
|
|
"""Mock UniFi Protect Chime device."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
Chime.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_chime.json", DOMAIN)
|
|
yield Chime.from_unifi_dict(**data)
|
|
|
|
Chime.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="aiport")
|
|
def aiport_fixture():
|
|
"""Mock UniFi Protect AI Port device."""
|
|
|
|
# disable pydantic validation so mocking can happen
|
|
AiPort.model_config["validate_assignment"] = False
|
|
|
|
data = load_json_object_fixture("sample_aiport.json", DOMAIN)
|
|
yield AiPort.from_unifi_dict(**data)
|
|
|
|
AiPort.model_config["validate_assignment"] = True
|
|
|
|
|
|
@pytest.fixture(name="fixed_now")
|
|
def fixed_now_fixture():
|
|
"""Return datetime object that will be consistent throughout test."""
|
|
return dt_util.utcnow()
|
|
|
|
|
|
@pytest.fixture(name="ufp_reauth_entry")
|
|
def mock_ufp_reauth_entry():
|
|
"""Mock the unifiprotect config entry for reauth and reconfigure tests."""
|
|
return MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_HOST: DEFAULT_HOST,
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: DEFAULT_PASSWORD,
|
|
CONF_API_KEY: DEFAULT_API_KEY,
|
|
"id": "UnifiProtect",
|
|
CONF_PORT: DEFAULT_PORT,
|
|
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
|
},
|
|
unique_id=_async_unifi_mac_from_hass(MAC_ADDR),
|
|
)
|
|
|
|
|
|
@pytest.fixture(name="ufp_reauth_entry_alt")
|
|
def mock_ufp_reauth_entry_alt():
|
|
"""Mock the unifiprotect config entry with alt port/SSL for reauth tests."""
|
|
return MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_HOST: DEFAULT_HOST,
|
|
CONF_USERNAME: DEFAULT_USERNAME,
|
|
CONF_PASSWORD: DEFAULT_PASSWORD,
|
|
CONF_API_KEY: DEFAULT_API_KEY,
|
|
"id": "UnifiProtect",
|
|
CONF_PORT: 8443,
|
|
CONF_VERIFY_SSL: True,
|
|
},
|
|
unique_id=_async_unifi_mac_from_hass(MAC_ADDR),
|
|
)
|
|
|
|
|
|
@pytest.fixture(name="ufp_public_only_entry")
|
|
def mock_ufp_public_only_entry():
|
|
"""Mock a public-API-only (API key, no local user) config entry."""
|
|
return MockConfigEntry(
|
|
domain=DOMAIN,
|
|
data={
|
|
CONF_HOST: DEFAULT_HOST,
|
|
CONF_API_KEY: DEFAULT_API_KEY,
|
|
"id": "UnifiProtect",
|
|
CONF_PORT: DEFAULT_PORT,
|
|
CONF_VERIFY_SSL: DEFAULT_VERIFY_SSL,
|
|
CONF_CONNECTION_MODE: CONNECTION_MODE_API_KEY_ONLY,
|
|
},
|
|
version=2,
|
|
unique_id=UNIFI_MAC,
|
|
)
|
|
|
|
|
|
@pytest.fixture(name="ufp_public_only_client")
|
|
def mock_ufp_public_only_client() -> Mock:
|
|
"""Mock an API-key-only ProtectApiClient for setup."""
|
|
client = Mock()
|
|
# Reading ``bootstrap`` on an API-key-only client raises in the library;
|
|
# mirroring that makes every accidental private-bootstrap read in a
|
|
# public-only code path fail loudly. A property needs the instance type,
|
|
# which mock gives each Mock of its own.
|
|
type(client).bootstrap = PropertyMock(
|
|
side_effect=BadRequest("Client not initialized, run `update` first")
|
|
)
|
|
client.is_public_only = True
|
|
client.has_public_bootstrap = True
|
|
|
|
meta = Mock()
|
|
meta.version = Version("7.2.105")
|
|
client.get_meta_info = AsyncMock(return_value=meta)
|
|
client.update_public = AsyncMock()
|
|
client.update = AsyncMock(side_effect=PublicOnlyModeError("public-only"))
|
|
client.get_bootstrap = AsyncMock(side_effect=PublicOnlyModeError("public-only"))
|
|
client.async_disconnect_ws = AsyncMock()
|
|
|
|
arm_mode = Mock(spec=NvrArmMode)
|
|
arm_mode.status = NvrArmModeStatus.DISABLED
|
|
nvr = Mock(spec=PublicNVR)
|
|
# The library backfills the mac during update_public(); it is present here.
|
|
nvr.mac = UNIFI_MAC
|
|
nvr.name = "Test NVR"
|
|
nvr.display_name = "Test NVR"
|
|
nvr.device_type = "UNVR4" # present on firmware newer than 7.1
|
|
nvr.type = "UNVR4"
|
|
nvr.id = "nvr-id"
|
|
nvr.model = ModelType.NVR
|
|
pb = Mock(spec=PublicBootstrap)
|
|
pb.nvr = nvr
|
|
pb.arm_mode = arm_mode
|
|
# One map per device family forwarded in public-only mode; tests replace
|
|
# them, so both helpers below read the attribute at call time.
|
|
pb.cameras = {}
|
|
pb.lights = {}
|
|
device_maps = {ModelType.CAMERA: "cameras", ModelType.LIGHT: "lights"}
|
|
|
|
def _all_devices(*, include_nvr: bool = False) -> Iterator[Mock]:
|
|
if include_nvr and pb.nvr is not None:
|
|
yield pb.nvr
|
|
for attr in device_maps.values():
|
|
yield from getattr(pb, attr).values()
|
|
|
|
def _get(model: ModelType, obj_id: str) -> Mock | None:
|
|
if (attr := device_maps.get(model)) is None:
|
|
return None
|
|
return getattr(pb, attr).get(obj_id)
|
|
|
|
pb.all_devices = _all_devices
|
|
pb.get = Mock(side_effect=_get)
|
|
client.public_bootstrap = pb
|
|
return client
|
|
|
|
|
|
@pytest.fixture(name="ufp_public_only")
|
|
def mock_ufp_public_only(
|
|
ufp_public_only_entry: MockConfigEntry,
|
|
ufp_public_only_client: Mock,
|
|
) -> MockUFPFixture:
|
|
"""Bundle the API-key-only entry and client, like ``ufp`` does for hybrid."""
|
|
ufp = MockUFPFixture(ufp_public_only_entry, ufp_public_only_client)
|
|
|
|
def subscribe_devices_websocket(
|
|
ws_callback: Callable[[WSSubscriptionMessage], None],
|
|
) -> Any:
|
|
ufp.devices_ws_subscription = ws_callback
|
|
return Mock()
|
|
|
|
def subscribe_devices_websocket_state(
|
|
ws_state_subscription: Callable[[WebsocketState], None],
|
|
) -> Any:
|
|
ufp.devices_ws_state_subscription = ws_state_subscription
|
|
return Mock()
|
|
|
|
def subscribe_events(events_callback: Callable[..., None]) -> Any:
|
|
ufp.events_subscription = events_callback
|
|
return Mock()
|
|
|
|
def subscribe_events_websocket_state(
|
|
ws_state_subscription: Callable[[WebsocketState], None],
|
|
) -> Any:
|
|
ufp.events_ws_state_subscription = ws_state_subscription
|
|
return Mock()
|
|
|
|
ufp_public_only_client.subscribe_devices_websocket = subscribe_devices_websocket
|
|
ufp_public_only_client.subscribe_devices_websocket_state = (
|
|
subscribe_devices_websocket_state
|
|
)
|
|
ufp_public_only_client.subscribe_events = subscribe_events
|
|
ufp_public_only_client.subscribe_events_websocket_state = (
|
|
subscribe_events_websocket_state
|
|
)
|
|
return ufp
|
|
|
|
|
|
@pytest.fixture(name="setup_public_only")
|
|
def setup_public_only_fixture(
|
|
hass: HomeAssistant,
|
|
ufp_public_only: MockUFPFixture,
|
|
) -> Callable[[], Coroutine[Any, Any, None]]:
|
|
"""Return a callable setting up the API-key-only entry with its mock client.
|
|
|
|
Deferred rather than an auto-setup fixture because several tests mutate the
|
|
client (or the entry) first. The entry is added here so those tests can.
|
|
"""
|
|
ufp_public_only.entry.add_to_hass(hass)
|
|
|
|
async def _setup() -> None:
|
|
with patch(
|
|
"homeassistant.components.unifiprotect.async_create_api_client",
|
|
return_value=ufp_public_only.api,
|
|
):
|
|
await hass.config_entries.async_setup(ufp_public_only.entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
return _setup
|
|
|
|
|
|
@pytest.fixture(name="mock_setup")
|
|
def mock_setup_fixture() -> Generator[AsyncMock]:
|
|
"""Mock async_setup and async_setup_entry to prevent reload issues in tests."""
|
|
with (
|
|
patch(
|
|
"homeassistant.components.unifiprotect.async_setup",
|
|
return_value=True,
|
|
),
|
|
patch(
|
|
"homeassistant.components.unifiprotect.async_setup_entry",
|
|
return_value=True,
|
|
) as mock,
|
|
):
|
|
yield mock
|
|
|
|
|
|
@pytest.fixture(name="mock_api_bootstrap")
|
|
def mock_api_bootstrap_fixture(bootstrap: Bootstrap):
|
|
"""Mock the ProtectApiClient.get_bootstrap method."""
|
|
with patch(
|
|
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_bootstrap",
|
|
return_value=bootstrap,
|
|
) as mock:
|
|
yield mock
|
|
|
|
|
|
@pytest.fixture(name="mock_api_meta_info")
|
|
def mock_api_meta_info_fixture():
|
|
"""Mock the ProtectApiClient.get_meta_info method."""
|
|
with patch(
|
|
"homeassistant.components.unifiprotect.config_flow.ProtectApiClient.get_meta_info",
|
|
return_value=None,
|
|
) as mock:
|
|
yield mock
|
|
|
|
|
|
@pytest.fixture(name="cloud_account")
|
|
def cloud_account() -> CloudAccount:
|
|
"""Return UI Cloud Account."""
|
|
|
|
return CloudAccount(
|
|
id="42",
|
|
first_name="Test",
|
|
last_name="User",
|
|
email="test@example.com",
|
|
user_id="42",
|
|
name="Test User",
|
|
location=None,
|
|
profile_img=None,
|
|
)
|