From 31ae121645f6d2fa4475ac3d5b03c0900979c176 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 9 Apr 2021 10:56:53 +0200 Subject: [PATCH] Add fixtures for Axis rtsp client and adapt tests to use them (#47901) * Add a fixture for rtsp client and adapt tests to use it * Better fixtures for RTSP events and signals --- homeassistant/components/axis/__init__.py | 4 +- homeassistant/components/axis/device.py | 4 +- tests/components/axis/conftest.py | 112 +++++++++++++++++++- tests/components/axis/test_binary_sensor.py | 51 ++++----- tests/components/axis/test_device.py | 40 +++++-- tests/components/axis/test_light.py | 54 +++++----- tests/components/axis/test_switch.py | 55 ++++++---- 7 files changed, 230 insertions(+), 90 deletions(-) diff --git a/homeassistant/components/axis/__init__.py b/homeassistant/components/axis/__init__.py index 378d02bcccd..acbdc2ca782 100644 --- a/homeassistant/components/axis/__init__.py +++ b/homeassistant/components/axis/__init__.py @@ -26,7 +26,9 @@ async def async_setup_entry(hass, config_entry): await device.async_update_device_registry() - hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + device.listeners.append( + hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, device.shutdown) + ) return True diff --git a/homeassistant/components/axis/device.py b/homeassistant/components/axis/device.py index f732ad2fb5d..93b63b64122 100644 --- a/homeassistant/components/axis/device.py +++ b/homeassistant/components/axis/device.py @@ -263,9 +263,7 @@ class AxisNetworkDevice: def disconnect_from_stream(self): """Stop stream.""" if self.api.stream.state != STATE_STOPPED: - self.api.stream.connection_status_callback.remove( - self.async_connection_status_callback - ) + self.api.stream.connection_status_callback.clear() self.api.stream.stop() async def shutdown(self, event): diff --git a/tests/components/axis/conftest.py b/tests/components/axis/conftest.py index b3964663767..be448359366 100644 --- a/tests/components/axis/conftest.py +++ b/tests/components/axis/conftest.py @@ -1,2 +1,112 @@ -"""axis conftest.""" +"""Axis conftest.""" + +from typing import Optional +from unittest.mock import patch + +from axis.rtsp import ( + SIGNAL_DATA, + SIGNAL_FAILED, + SIGNAL_PLAYING, + STATE_PLAYING, + STATE_STOPPED, +) +import pytest + from tests.components.light.conftest import mock_light_profiles # noqa: F401 + + +@pytest.fixture(autouse=True) +def mock_axis_rtspclient(): + """No real RTSP communication allowed.""" + with patch("axis.streammanager.RTSPClient") as rtsp_client_mock: + + rtsp_client_mock.return_value.session.state = STATE_STOPPED + + async def start_stream(): + """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(): + """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: Optional[dict] = None, state: str = ""): + """Generate a RTSP call.""" + axis_streammanager_session_callback = rtsp_client_mock.call_args[0][4] + + if data: + rtsp_client_mock.return_value.rtp.data = data + axis_streammanager_session_callback(signal=SIGNAL_DATA) + elif state: + axis_streammanager_session_callback(signal=state) + else: + raise NotImplementedError + + yield make_rtsp_call + + +@pytest.fixture(autouse=True) +def mock_rtsp_event(mock_axis_rtspclient): + """Fixture to allow mocking received RTSP events.""" + + def send_event( + topic: str, + data_type: str, + data_value: str, + operation: str = "Initialized", + source_name: str = "", + source_idx: str = "", + ) -> None: + source = "" + if source_name != "" and source_idx != "": + source = f'' + + event = f""" + + + + + {topic} + + + + uri://bf32a3b9-e5e7-4d57-a48d-1b5be9ae7b16/ProducerReference + + + + + {source} + + + + + + + + + +""" + + mock_axis_rtspclient(data=event.encode("utf-8")) + + yield send_event + + +@pytest.fixture(autouse=True) +def mock_rtsp_signal_state(mock_axis_rtspclient): + """Fixture to allow mocking RTSP state signalling.""" + + def send_signal(connected: bool) -> None: + """Signal state change of RTSP connection.""" + signal = SIGNAL_PLAYING if connected else SIGNAL_FAILED + mock_axis_rtspclient(state=signal) + + yield send_signal diff --git a/tests/components/axis/test_binary_sensor.py b/tests/components/axis/test_binary_sensor.py index 98ef55282c3..2429ec61855 100644 --- a/tests/components/axis/test_binary_sensor.py +++ b/tests/components/axis/test_binary_sensor.py @@ -10,31 +10,6 @@ from homeassistant.setup import async_setup_component from .test_device import NAME, setup_axis_integration -EVENTS = [ - { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Sensor/PIR", - "source": "sensor", - "source_idx": "0", - "type": "state", - "value": "0", - }, - { - "operation": "Initialized", - "topic": "tns1:PTZController/tnsaxis:PTZPresets/Channel_1", - "source": "PresetToken", - "source_idx": "0", - "type": "on_preset", - "value": "1", - }, - { - "operation": "Initialized", - "topic": "tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", - "type": "active", - "value": "1", - }, -] - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -57,12 +32,30 @@ async def test_no_binary_sensors(hass): assert not hass.states.async_entity_ids(BINARY_SENSOR_DOMAIN) -async def test_binary_sensors(hass): +async def test_binary_sensors(hass, mock_rtsp_event): """Test that sensors are loaded properly.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + await setup_axis_integration(hass) - device.api.event.update(EVENTS) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Sensor/PIR", + data_type="state", + data_value="0", + source_name="sensor", + source_idx="0", + ) + mock_rtsp_event( + topic="tnsaxis:CameraApplicationPlatform/VMD/Camera1Profile1", + data_type="active", + data_value="1", + ) + # Unsupported event + 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)) == 2 diff --git a/tests/components/axis/test_device.py b/tests/components/axis/test_device.py index a5371395638..cb6e5b1a12b 100644 --- a/tests/components/axis/test_device.py +++ b/tests/components/axis/test_device.py @@ -23,7 +23,9 @@ from homeassistant.const import ( CONF_PASSWORD, CONF_PORT, CONF_USERNAME, + STATE_OFF, STATE_ON, + STATE_UNAVAILABLE, ) from tests.common import MockConfigEntry, async_fire_mqtt_message @@ -288,7 +290,7 @@ async def setup_axis_integration(hass, config=ENTRY_CONFIG, options=ENTRY_OPTION ) config_entry.add_to_hass(hass) - with patch("axis.rtsp.RTSPClient.start", return_value=True), respx.mock: + with respx.mock: mock_default_vapix_requests(respx) await hass.config_entries.async_setup(config_entry.entry_id) await hass.async_block_till_done() @@ -389,12 +391,38 @@ async def test_update_address(hass): assert len(mock_setup_entry.mock_calls) == 1 -async def test_device_unavailable(hass): +async def test_device_unavailable(hass, mock_rtsp_event, mock_rtsp_signal_state): """Successful setup.""" - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] - device.async_connection_status_callback(status=False) - assert not device.available + await setup_axis_integration(hass) + + # Provide an entity that can be used to verify connection state on + mock_rtsp_event( + topic="tns1:AudioSource/tnsaxis:TriggerLevel", + data_type="triggered", + data_value="10", + source_name="channel", + source_idx="1", + ) + await hass.async_block_till_done() + + assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF + + # Connection to device has failed + + mock_rtsp_signal_state(connected=False) + await hass.async_block_till_done() + + assert ( + hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state + == STATE_UNAVAILABLE + ) + + # Connection to device has been restored + + mock_rtsp_signal_state(connected=True) + await hass.async_block_till_done() + + assert hass.states.get(f"{BINARY_SENSOR_DOMAIN}.{NAME}_sound_1").state == STATE_OFF async def test_device_reset(hass): diff --git a/tests/components/axis/test_light.py b/tests/components/axis/test_light.py index db4ba86ceae..db7ca6921fb 100644 --- a/tests/components/axis/test_light.py +++ b/tests/components/axis/test_light.py @@ -27,24 +27,6 @@ API_DISCOVERY_LIGHT_CONTROL = { "name": "Light Control", } -EVENT_ON = { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Light/Status", - "source": "id", - "source_idx": "0", - "type": "state", - "value": "ON", -} - -EVENT_OFF = { - "operation": "Initialized", - "topic": "tns1:Device/tnsaxis:Light/Status", - "source": "id", - "source_idx": "0", - "type": "state", - "value": "OFF", -} - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -62,7 +44,9 @@ async def test_no_lights(hass): assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_no_light_entity_without_light_control_representation(hass): +async def test_no_light_entity_without_light_control_representation( + hass, mock_rtsp_event +): """Verify no lights entities get created without light control representation.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) @@ -73,23 +57,27 @@ async def test_no_light_entity_without_light_control_representation(hass): with patch.dict(API_DISCOVERY_RESPONSE, api_discovery), patch.dict( LIGHT_CONTROL_RESPONSE, light_control ): - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + await setup_axis_integration(hass) - device.api.event.update([EVENT_ON]) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Light/Status", + data_type="state", + data_value="ON", + source_name="id", + source_idx="0", + ) await hass.async_block_till_done() assert not hass.states.async_entity_ids(LIGHT_DOMAIN) -async def test_lights(hass): +async def test_lights(hass, mock_rtsp_event): """Test that lights are loaded properly.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_LIGHT_CONTROL) with patch.dict(API_DISCOVERY_RESPONSE, api_discovery): - config_entry = await setup_axis_integration(hass) - device = hass.data[AXIS_DOMAIN][config_entry.unique_id] + await setup_axis_integration(hass) # Add light with patch( @@ -99,7 +87,13 @@ async def test_lights(hass): "axis.light_control.LightControl.get_valid_intensity", return_value={"data": {"ranges": [{"high": 150}]}}, ): - device.api.event.update([EVENT_ON]) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Light/Status", + data_type="state", + data_value="ON", + source_name="id", + source_idx="0", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(LIGHT_DOMAIN)) == 1 @@ -144,7 +138,13 @@ async def test_lights(hass): mock_deactivate.assert_called_once() # Event turn off light - device.api.event.update([EVENT_OFF]) + mock_rtsp_event( + topic="tns1:Device/tnsaxis:Light/Status", + data_type="state", + data_value="OFF", + source_name="id", + source_idx="0", + ) await hass.async_block_till_done() light_0 = hass.states.get(entity_id) diff --git a/tests/components/axis/test_switch.py b/tests/components/axis/test_switch.py index dcbe285cb54..541c377d3ff 100644 --- a/tests/components/axis/test_switch.py +++ b/tests/components/axis/test_switch.py @@ -21,25 +21,6 @@ from .test_device import ( setup_axis_integration, ) -EVENTS = [ - { - "operation": "Initialized", - "topic": "tns1:Device/Trigger/Relay", - "source": "RelayToken", - "source_idx": "0", - "type": "LogicalState", - "value": "inactive", - }, - { - "operation": "Initialized", - "topic": "tns1:Device/Trigger/Relay", - "source": "RelayToken", - "source_idx": "1", - "type": "LogicalState", - "value": "active", - }, -] - async def test_platform_manually_configured(hass): """Test that nothing happens when platform is manually configured.""" @@ -57,7 +38,7 @@ async def test_no_switches(hass): assert not hass.states.async_entity_ids(SWITCH_DOMAIN) -async def test_switches_with_port_cgi(hass): +async def test_switches_with_port_cgi(hass, mock_rtsp_event): """Test that switches are loaded properly using port.cgi.""" config_entry = await setup_axis_integration(hass) device = hass.data[AXIS_DOMAIN][config_entry.unique_id] @@ -68,7 +49,20 @@ async def test_switches_with_port_cgi(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - device.api.event.update(EVENTS) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="inactive", + source_name="RelayToken", + source_idx="0", + ) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="1", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2 @@ -100,7 +94,9 @@ async def test_switches_with_port_cgi(hass): device.api.vapix.ports["0"].open.assert_called_once() -async def test_switches_with_port_management(hass): +async def test_switches_with_port_management( + hass, mock_axis_rtspclient, mock_rtsp_event +): """Test that switches are loaded properly using port management.""" api_discovery = deepcopy(API_DISCOVERY_RESPONSE) api_discovery["data"]["apiList"].append(API_DISCOVERY_PORT_MANAGEMENT) @@ -115,7 +111,20 @@ async def test_switches_with_port_management(hass): device.api.vapix.ports["0"].close = AsyncMock() device.api.vapix.ports["1"].name = "" - device.api.event.update(EVENTS) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="inactive", + source_name="RelayToken", + source_idx="0", + ) + mock_rtsp_event( + topic="tns1:Device/Trigger/Relay", + data_type="LogicalState", + data_value="active", + source_name="RelayToken", + source_idx="1", + ) await hass.async_block_till_done() assert len(hass.states.async_entity_ids(SWITCH_DOMAIN)) == 2