Improve axis tests (#114035)

* Combine binary sensor tests into more logical groups

* Improve light tests

* Clean up switch tests

* Improve typing in conftest

* Add typing to camera

* Improve binary sensor

* Improve light

* Improve switch
This commit is contained in:
Robert Svensson 2024-03-23 00:44:06 +01:00 committed by GitHub
parent 26b6bd83fc
commit bf8d880e5f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 142 additions and 159 deletions

View File

@ -2,8 +2,10 @@
from __future__ import annotations
from collections.abc import Generator
from collections.abc import Callable, Generator
from copy import deepcopy
from types import MappingProxyType
from typing import Any
from unittest.mock import AsyncMock, patch
from axis.rtsp import Signal, State
@ -11,6 +13,7 @@ import pytest
import respx
from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
CONF_HOST,
CONF_MODEL,
@ -19,6 +22,7 @@ from homeassistant.const import (
CONF_PORT,
CONF_USERNAME,
)
from homeassistant.core import HomeAssistant
from .const import (
API_DISCOVERY_RESPONSE,
@ -42,7 +46,6 @@ from .const import (
)
from tests.common import MockConfigEntry
from tests.components.light.conftest import mock_light_profiles # noqa: F401
@pytest.fixture
@ -58,28 +61,33 @@ def mock_setup_entry() -> Generator[AsyncMock, None, None]:
@pytest.fixture(name="config_entry")
def config_entry_fixture(hass, config, options, config_entry_version):
def config_entry_fixture(
hass: HomeAssistant,
config_entry_data: MappingProxyType[str, Any],
config_entry_options: MappingProxyType[str, Any],
config_entry_version: int,
) -> ConfigEntry:
"""Define a config entry fixture."""
entry = MockConfigEntry(
config_entry = MockConfigEntry(
domain=AXIS_DOMAIN,
entry_id="676abe5b73621446e6550a2e86ffe3dd",
unique_id=FORMATTED_MAC,
data=config,
options=options,
data=config_entry_data,
options=config_entry_options,
version=config_entry_version,
)
entry.add_to_hass(hass)
return entry
config_entry.add_to_hass(hass)
return config_entry
@pytest.fixture(name="config_entry_version")
def config_entry_version_fixture(request):
def config_entry_version_fixture() -> int:
"""Define a config entry version fixture."""
return 3
@pytest.fixture(name="config")
def config_fixture():
@pytest.fixture(name="config_entry_data")
def config_entry_data_fixture() -> MappingProxyType[str, Any]:
"""Define a config entry data fixture."""
return {
CONF_HOST: DEFAULT_HOST,
@ -91,8 +99,8 @@ def config_fixture():
}
@pytest.fixture(name="options")
def options_fixture(request):
@pytest.fixture(name="config_entry_options")
def config_entry_options_fixture() -> MappingProxyType[str, Any]:
"""Define a config entry options fixture."""
return {}
@ -102,11 +110,14 @@ def options_fixture(request):
@pytest.fixture(name="mock_vapix_requests")
def default_request_fixture(
respx_mock, port_management_payload, param_properties_payload, param_ports_payload
):
respx_mock: respx,
port_management_payload: dict[str, Any],
param_properties_payload: dict[str, Any],
param_ports_payload: dict[str, Any],
) -> Callable[[str], None]:
"""Mock default Vapix requests responses."""
def __mock_default_requests(host):
def __mock_default_requests(host: str) -> None:
respx_mock(base_url=f"http://{host}:80")
if host != DEFAULT_HOST:
@ -196,13 +207,13 @@ def default_request_fixture(
@pytest.fixture
def api_discovery_items():
def api_discovery_items() -> dict[str, Any]:
"""Additional Apidiscovery items."""
return {}
@pytest.fixture(autouse=True)
def api_discovery_fixture(api_discovery_items):
def api_discovery_fixture(api_discovery_items: dict[str, Any]) -> None:
"""Apidiscovery mock response."""
data = deepcopy(API_DISCOVERY_RESPONSE)
if api_discovery_items:
@ -211,34 +222,36 @@ def api_discovery_fixture(api_discovery_items):
@pytest.fixture(name="port_management_payload")
def io_port_management_data_fixture():
def io_port_management_data_fixture() -> dict[str, Any]:
"""Property parameter data."""
return PORT_MANAGEMENT_RESPONSE
@pytest.fixture(name="param_properties_payload")
def param_properties_data_fixture():
def param_properties_data_fixture() -> dict[str, Any]:
"""Property parameter data."""
return PROPERTIES_RESPONSE
@pytest.fixture(name="param_ports_payload")
def param_ports_data_fixture():
def param_ports_data_fixture() -> dict[str, Any]:
"""Property parameter data."""
return PORTS_RESPONSE
@pytest.fixture(name="setup_default_vapix_requests")
def default_vapix_requests_fixture(mock_vapix_requests):
def default_vapix_requests_fixture(mock_vapix_requests: Callable[[str], None]) -> None:
"""Mock default Vapix requests responses."""
mock_vapix_requests(DEFAULT_HOST)
@pytest.fixture(name="prepare_config_entry")
async def prep_config_entry_fixture(hass, config_entry, setup_default_vapix_requests):
async def prep_config_entry_fixture(
hass: HomeAssistant, config_entry: ConfigEntry, setup_default_vapix_requests: None
) -> Callable[[], ConfigEntry]:
"""Fixture factory to set up Axis network device."""
async def __mock_setup_config_entry():
async def __mock_setup_config_entry() -> ConfigEntry:
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
return config_entry
@ -247,7 +260,9 @@ async def prep_config_entry_fixture(hass, config_entry, setup_default_vapix_requ
@pytest.fixture(name="setup_config_entry")
async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_requests):
async def setup_config_entry_fixture(
hass: HomeAssistant, config_entry: ConfigEntry, setup_default_vapix_requests: None
) -> ConfigEntry:
"""Define a fixture to set up Axis network device."""
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
@ -258,24 +273,24 @@ async def setup_config_entry_fixture(hass, config_entry, setup_default_vapix_req
@pytest.fixture(autouse=True)
def mock_axis_rtspclient():
def mock_axis_rtspclient() -> Generator[Callable[[dict | None, str], None], None, None]:
"""No real RTSP communication allowed."""
with patch("axis.stream_manager.RTSPClient") as rtsp_client_mock:
rtsp_client_mock.return_value.session.state = State.STOPPED
async def start_stream():
async def start_stream() -> None:
"""Set state to playing when calling RTSPClient.start."""
rtsp_client_mock.return_value.session.state = State.PLAYING
rtsp_client_mock.return_value.start = start_stream
def stop_stream():
def stop_stream() -> None:
"""Set state to stopped when calling RTSPClient.stop."""
rtsp_client_mock.return_value.session.state = State.STOPPED
rtsp_client_mock.return_value.stop = stop_stream
def make_rtsp_call(data: dict | None = None, state: str = ""):
def make_rtsp_call(data: dict | None = None, state: str = "") -> None:
"""Generate a RTSP call."""
axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4]
@ -291,7 +306,9 @@ def mock_axis_rtspclient():
@pytest.fixture(autouse=True)
def mock_rtsp_event(mock_axis_rtspclient):
def mock_rtsp_event(
mock_axis_rtspclient: Callable[[dict | None, str], None],
) -> Callable[[str, str, str, str, str, str], None]:
"""Fixture to allow mocking received RTSP events."""
def send_event(
@ -342,7 +359,9 @@ def mock_rtsp_event(mock_axis_rtspclient):
@pytest.fixture(autouse=True)
def mock_rtsp_signal_state(mock_axis_rtspclient):
def mock_rtsp_signal_state(
mock_axis_rtspclient: Callable[[dict | None, str], None],
) -> Callable[[bool], None]:
"""Fixture to allow mocking RTSP state signalling."""
def send_signal(connected: bool) -> None:

View File

@ -1,54 +1,20 @@
"""Axis binary sensor platform tests."""
from collections.abc import Callable
import pytest
from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN
from homeassistant.components.binary_sensor import (
DOMAIN as BINARY_SENSOR_DOMAIN,
BinarySensorDeviceClass,
)
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_OFF, STATE_ON
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import NAME
async def test_platform_manually_configured(hass: HomeAssistant) -> None:
"""Test that nothing happens when platform is manually configured."""
assert (
await async_setup_component(
hass,
BINARY_SENSOR_DOMAIN,
{BINARY_SENSOR_DOMAIN: {"platform": AXIS_DOMAIN}},
)
is True
)
assert AXIS_DOMAIN not in hass.data
async def test_no_binary_sensors(hass: HomeAssistant, setup_config_entry) -> None:
"""Test that no sensors in Axis results in no sensor entities."""
assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)
async def test_unsupported_binary_sensors(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event
) -> None:
"""Test that unsupported sensors are not loaded."""
mock_rtsp_event(
topic="tns1:PTZController/tnsaxis:PTZPresets/Channel_1",
data_type="on_preset",
data_value="1",
source_name="PresetToken",
source_idx="0",
)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0
@pytest.mark.parametrize(
("event", "entity"),
[
@ -178,10 +144,41 @@ async def test_unsupported_binary_sensors(
"device_class": BinarySensorDeviceClass.MOTION,
},
),
# Events with names generated from event ID and topic
(
{
"topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9",
"data_type": "active",
"data_value": "1",
},
{
"id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_vmd4_camera1profile9",
"state": STATE_ON,
"name": f"{NAME} VMD4 Camera1Profile9",
"device_class": BinarySensorDeviceClass.MOTION,
},
),
(
{
"topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8",
"data_type": "active",
"data_value": "1",
},
{
"id": f"{BINARY_SENSOR_DOMAIN}.{NAME}_object_analytics_device1scenario8",
"state": STATE_ON,
"name": f"{NAME} Object Analytics Device1Scenario8",
"device_class": BinarySensorDeviceClass.MOTION,
},
),
],
)
async def test_binary_sensors(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event, event, entity
hass: HomeAssistant,
setup_config_entry: ConfigEntry,
mock_rtsp_event: Callable[[str, str, str, str, str, str], None],
event: dict[str, str],
entity: dict[str, str],
) -> None:
"""Test that sensors are loaded properly."""
mock_rtsp_event(**event)
@ -198,6 +195,15 @@ async def test_binary_sensors(
@pytest.mark.parametrize(
("event"),
[
# Event with unsupported topic
{
"topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1",
"data_type": "on_preset",
"data_value": "1",
"source_name": "PresetToken",
"source_idx": "0",
},
# Event with unsupported source_idx
{
"topic": "tns1:Device/tnsaxis:IO/Port",
"data_type": "state",
@ -206,6 +212,7 @@ async def test_binary_sensors(
"source_name": "port",
"source_idx": "-1",
},
# Event with unsupported ID in topic 'ANY'
{
"topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1ProfileANY",
"data_type": "active",
@ -219,40 +226,12 @@ async def test_binary_sensors(
],
)
async def test_unsupported_events(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event, event
hass: HomeAssistant,
setup_config_entry: ConfigEntry,
mock_rtsp_event: Callable[[str, str, str, str, str, str], None],
event: dict[str, str],
) -> None:
"""Validate nothing breaks with unsupported events."""
mock_rtsp_event(**event)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 0
@pytest.mark.parametrize(
("event", "entity_id"),
[
(
{
"topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile9",
"data_type": "active",
"data_value": "1",
},
"binary_sensor.name_vmd4_camera1profile9",
),
(
{
"topic": "tnsaxis:CameraApplicationPlatform/ObjectAnalytics/Device1Scenario8",
"data_type": "active",
"data_value": "1",
},
"binary_sensor.name_object_analytics_device1scenario8",
),
],
)
async def test_no_primary_name_for_event(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event, event, entity_id
) -> None:
"""Validate fallback method for getting name works."""
mock_rtsp_event(**event)
await hass.async_block_till_done()
assert len(hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN)) == 1
assert hass.states.get(entity_id)

View File

@ -1,5 +1,7 @@
"""Axis camera platform tests."""
from collections.abc import Callable
import pytest
from homeassistant.components import camera
@ -8,6 +10,7 @@ from homeassistant.components.axis.const import (
DOMAIN as AXIS_DOMAIN,
)
from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import STATE_IDLE
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
@ -27,7 +30,7 @@ async def test_platform_manually_configured(hass: HomeAssistant) -> None:
assert AXIS_DOMAIN not in hass.data
async def test_camera(hass: HomeAssistant, setup_config_entry) -> None:
async def test_camera(hass: HomeAssistant, setup_config_entry: ConfigEntry) -> None:
"""Test that Axis camera platform is loaded properly."""
assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1
@ -46,9 +49,9 @@ async def test_camera(hass: HomeAssistant, setup_config_entry) -> None:
)
@pytest.mark.parametrize("options", [{CONF_STREAM_PROFILE: "profile_1"}])
@pytest.mark.parametrize("config_entry_options", [{CONF_STREAM_PROFILE: "profile_1"}])
async def test_camera_with_stream_profile(
hass: HomeAssistant, setup_config_entry
hass: HomeAssistant, setup_config_entry: ConfigEntry
) -> None:
"""Test that Axis camera entity is using the correct path with stream profike."""
assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 1
@ -83,7 +86,9 @@ root.Properties.System.SerialNumber={MAC}
@pytest.mark.parametrize("param_properties_payload", [property_data])
async def test_camera_disabled(hass: HomeAssistant, prepare_config_entry) -> None:
async def test_camera_disabled(
hass: HomeAssistant, prepare_config_entry: Callable[[], ConfigEntry]
) -> None:
"""Test that Axis camera platform is loaded properly but does not create camera entity."""
await prepare_config_entry()
assert len(hass.states.async_entity_ids(CAMERA_DOMAIN)) == 0

View File

@ -45,7 +45,7 @@ def hass_mock_forward_entry_setup(hass):
async def test_device_setup(
hass: HomeAssistant,
forward_entry_setup,
config,
config_entry_data,
setup_config_entry,
device_registry: dr.DeviceRegistry,
) -> None:
@ -63,9 +63,9 @@ async def test_device_setup(
assert forward_entry_setup.mock_calls[2][1][1] == "light"
assert forward_entry_setup.mock_calls[3][1][1] == "switch"
assert hub.config.host == config[CONF_HOST]
assert hub.config.model == config[CONF_MODEL]
assert hub.config.name == config[CONF_NAME]
assert hub.config.host == config_entry_data[CONF_HOST]
assert hub.config.model == config_entry_data[CONF_MODEL]
assert hub.config.name == config_entry_data[CONF_NAME]
assert hub.unique_id == FORMATTED_MAC
device_entry = device_registry.async_get_device(
@ -206,11 +206,11 @@ async def test_device_unknown_error(
assert hass.data[AXIS_DOMAIN] == {}
async def test_shutdown(config) -> None:
async def test_shutdown(config_entry_data) -> None:
"""Successful shutdown."""
hass = Mock()
entry = Mock()
entry.data = config
entry.data = config_entry_data
mock_api = Mock()
mock_api.vapix.serial_number = FORMATTED_MAC
@ -221,25 +221,27 @@ async def test_shutdown(config) -> None:
assert len(axis_device.api.stream.stop.mock_calls) == 1
async def test_get_device_fails(hass: HomeAssistant, config) -> None:
async def test_get_device_fails(hass: HomeAssistant, config_entry_data) -> None:
"""Device unauthorized yields authentication required error."""
with patch(
"axis.vapix.vapix.Vapix.initialize", side_effect=axislib.Unauthorized
), pytest.raises(axis.errors.AuthenticationRequired):
await axis.hub.get_axis_api(hass, config)
await axis.hub.get_axis_api(hass, config_entry_data)
async def test_get_device_device_unavailable(hass: HomeAssistant, config) -> None:
async def test_get_device_device_unavailable(
hass: HomeAssistant, config_entry_data
) -> None:
"""Device unavailable yields cannot connect error."""
with patch(
"axis.vapix.vapix.Vapix.request", side_effect=axislib.RequestError
), pytest.raises(axis.errors.CannotConnect):
await axis.hub.get_axis_api(hass, config)
await axis.hub.get_axis_api(hass, config_entry_data)
async def test_get_device_unknown_error(hass: HomeAssistant, config) -> None:
async def test_get_device_unknown_error(hass: HomeAssistant, config_entry_data) -> None:
"""Device yield unknown error."""
with patch(
"axis.vapix.vapix.Vapix.request", side_effect=axislib.AxisException
), pytest.raises(axis.errors.AuthenticationRequired):
await axis.hub.get_axis_api(hass, config)
await axis.hub.get_axis_api(hass, config_entry_data)

View File

@ -1,13 +1,15 @@
"""Axis light platform tests."""
from collections.abc import Callable
from typing import Any
from unittest.mock import patch
from axis.vapix.models.api import CONTEXT
import pytest
import respx
from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN
from homeassistant.components.light import ATTR_BRIGHTNESS, DOMAIN as LIGHT_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
@ -16,7 +18,6 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import DEFAULT_HOST, NAME
@ -28,7 +29,7 @@ API_DISCOVERY_LIGHT_CONTROL = {
@pytest.fixture
def light_control_items():
def light_control_items() -> list[dict[str, Any]]:
"""Available lights."""
return [
{
@ -47,7 +48,7 @@ def light_control_items():
@pytest.fixture(autouse=True)
def light_control_fixture(light_control_items):
def light_control_fixture(light_control_items: list[dict[str, Any]]) -> None:
"""Light control mock response."""
data = {
"apiVersion": "1.1",
@ -67,24 +68,12 @@ def light_control_fixture(light_control_items):
)
async def test_platform_manually_configured(hass: HomeAssistant) -> None:
"""Test that nothing happens when platform is manually configured."""
assert await async_setup_component(
hass, LIGHT_DOMAIN, {LIGHT_DOMAIN: {"platform": AXIS_DOMAIN}}
)
assert AXIS_DOMAIN not in hass.data
async def test_no_lights(hass: HomeAssistant, setup_config_entry) -> None:
"""Test that no light events in Axis results in no light entities."""
assert not hass.states.async_entity_ids(LIGHT_DOMAIN)
@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL])
@pytest.mark.parametrize("light_control_items", [[]])
async def test_no_light_entity_without_light_control_representation(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event
hass: HomeAssistant,
setup_config_entry: ConfigEntry,
mock_rtsp_event: Callable[[str, str, str, str, str, str], None],
) -> None:
"""Verify no lights entities get created without light control representation."""
mock_rtsp_event(
@ -102,10 +91,10 @@ async def test_no_light_entity_without_light_control_representation(
@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_LIGHT_CONTROL])
async def test_lights(
hass: HomeAssistant,
respx_mock,
setup_config_entry,
mock_rtsp_event,
api_discovery_items,
respx_mock: respx,
setup_config_entry: ConfigEntry,
mock_rtsp_event: Callable[[str, str, str, str, str, str], None],
api_discovery_items: dict[str, Any],
) -> None:
"""Test that lights are loaded properly."""
# Add light

View File

@ -1,12 +1,13 @@
"""Axis switch platform tests."""
from collections.abc import Callable
from unittest.mock import patch
from axis.vapix.models.api import CONTEXT
import pytest
from homeassistant.components.axis.const import DOMAIN as AXIS_DOMAIN
from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN
from homeassistant.config_entries import ConfigEntry
from homeassistant.const import (
ATTR_ENTITY_ID,
SERVICE_TURN_OFF,
@ -15,25 +16,9 @@ from homeassistant.const import (
STATE_ON,
)
from homeassistant.core import HomeAssistant
from homeassistant.setup import async_setup_component
from .const import API_DISCOVERY_PORT_MANAGEMENT, NAME
async def test_platform_manually_configured(hass: HomeAssistant) -> None:
"""Test that nothing happens when platform is manually configured."""
assert await async_setup_component(
hass, SWITCH_DOMAIN, {SWITCH_DOMAIN: {"platform": AXIS_DOMAIN}}
)
assert AXIS_DOMAIN not in hass.data
async def test_no_switches(hass: HomeAssistant, setup_config_entry) -> None:
"""Test that no output events in Axis results in no switch entities."""
assert not hass.states.async_entity_ids(SWITCH_DOMAIN)
PORT_DATA = """root.IOPort.I0.Configurable=yes
root.IOPort.I0.Direction=output
root.IOPort.I0.Output.Name=Doorbell
@ -47,7 +32,9 @@ root.IOPort.I1.Output.Active=open
@pytest.mark.parametrize("param_ports_payload", [PORT_DATA])
async def test_switches_with_port_cgi(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event
hass: HomeAssistant,
setup_config_entry: ConfigEntry,
mock_rtsp_event: Callable[[str, str, str, str, str, str], None],
) -> None:
"""Test that switches are loaded properly using port.cgi."""
mock_rtsp_event(
@ -130,7 +117,9 @@ PORT_MANAGEMENT_RESPONSE = {
@pytest.mark.parametrize("api_discovery_items", [API_DISCOVERY_PORT_MANAGEMENT])
@pytest.mark.parametrize("port_management_payload", [PORT_MANAGEMENT_RESPONSE])
async def test_switches_with_port_management(
hass: HomeAssistant, setup_config_entry, mock_rtsp_event
hass: HomeAssistant,
setup_config_entry: ConfigEntry,
mock_rtsp_event: Callable[[str, str, str, str, str, str], None],
) -> None:
"""Test that switches are loaded properly using port management."""
mock_rtsp_event(