From 696288abf63c316daa7752b99b5072e286565102 Mon Sep 17 00:00:00 2001 From: Steven B <51370195+sdb9696@users.noreply.github.com> Date: Wed, 4 Sep 2024 16:59:41 +0100 Subject: [PATCH] Add tests --- .../components/ring/binary_sensor.py | 6 +- homeassistant/components/ring/strings.json | 10 +- tests/components/ring/common.py | 16 ++ tests/components/ring/conftest.py | 47 +---- tests/components/ring/device_mocks.py | 6 +- tests/components/ring/test_binary_sensor.py | 186 ++++++++++++++++-- tests/components/ring/test_event.py | 79 ++++++++ tests/components/ring/test_init.py | 37 +++- tests/components/ring/test_sensor.py | 7 +- 9 files changed, 333 insertions(+), 61 deletions(-) create mode 100644 tests/components/ring/test_event.py diff --git a/homeassistant/components/ring/binary_sensor.py b/homeassistant/components/ring/binary_sensor.py index 1973b3be5b29ca..45985fa17b460d 100644 --- a/homeassistant/components/ring/binary_sensor.py +++ b/homeassistant/components/ring/binary_sensor.py @@ -45,7 +45,7 @@ class RingBinarySensorEntityDescription( key=KIND_DING, translation_key=KIND_DING, device_class=BinarySensorDeviceClass.OCCUPANCY, - entity_registry_enabled_default=False, + entity_registry_enabled_default=True, capability=RingCapability.DING, deprecated_info=DeprecatedInfo( new_platform=Platform.EVENT, breaks_in_ha_version="2025.3.0" @@ -55,7 +55,7 @@ class RingBinarySensorEntityDescription( key=KIND_MOTION, translation_key=KIND_MOTION, device_class=BinarySensorDeviceClass.MOTION, - entity_registry_enabled_default=False, + entity_registry_enabled_default=True, capability=RingCapability.MOTION_DETECTION, deprecated_info=DeprecatedInfo( new_platform=Platform.EVENT, breaks_in_ha_version="2025.3.0" @@ -122,7 +122,7 @@ def _async_handle_event(self, alert: RingEvent) -> None: self._attr_is_on = True self._active_alert = alert loop = self.hass.loop - when = loop.time() + 60 # alert.expires_in + when = loop.time() + alert.expires_in if self._cancel_callback: self._cancel_callback.cancel() self._cancel_callback = loop.call_at(when, self._async_cancel_event) diff --git a/homeassistant/components/ring/strings.json b/homeassistant/components/ring/strings.json index f6898e0d3a54ef..308ffeda9c1ad1 100644 --- a/homeassistant/components/ring/strings.json +++ b/homeassistant/components/ring/strings.json @@ -32,6 +32,14 @@ } }, "entity": { + "binary_sensor": { + "ding": { + "name": "Ding" + }, + "motion": { + "name": "Motion" + } + }, "event": { "ding": { "name": "Ding" @@ -40,7 +48,7 @@ "name": "Motion" }, "intercom_unlock": { - "name": "Unlock" + "name": "Intercom unlock" } }, "button": { diff --git a/tests/components/ring/common.py b/tests/components/ring/common.py index 3b78adf0e091f4..a2e016efbed402 100644 --- a/tests/components/ring/common.py +++ b/tests/components/ring/common.py @@ -2,6 +2,7 @@ from unittest.mock import patch +from homeassistant.components.automation import DOMAIN as AUTOMATION_DOMAIN from homeassistant.components.ring import DOMAIN from homeassistant.const import Platform from homeassistant.core import HomeAssistant @@ -18,3 +19,18 @@ async def setup_platform(hass: HomeAssistant, platform: Platform) -> None: with patch("homeassistant.components.ring.PLATFORMS", [platform]): assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done(wait_background_tasks=True) + + +async def setup_automation(hass: HomeAssistant, alias: str, entity_id: str): + """Set up an automation for tests.""" + assert await async_setup_component( + hass, + AUTOMATION_DOMAIN, + { + AUTOMATION_DOMAIN: { + "alias": alias, + "trigger": {"platform": "state", "entity_id": entity_id, "to": "on"}, + "action": {"action": "notify.notify", "metadata": {}, "data": {}}, + } + }, + ) diff --git a/tests/components/ring/conftest.py b/tests/components/ring/conftest.py index 77bd4340573853..f91c2fb06a8cb6 100644 --- a/tests/components/ring/conftest.py +++ b/tests/components/ring/conftest.py @@ -2,7 +2,7 @@ from collections.abc import Generator from itertools import chain -from unittest.mock import AsyncMock, Mock, create_autospec, patch +from unittest.mock import AsyncMock, Mock, PropertyMock, create_autospec, patch import pytest import ring_doorbell @@ -138,42 +138,13 @@ async def mock_added_config_entry( @pytest.fixture(autouse=True) -def mock_listener(): - """Fixture to mock the push client connect and disconnect.""" +def mock_ring_event_listener_class(): + """Fixture to mock the ring event listener.""" - f = _FakeRingListener() with patch( - "homeassistant.components.ring.coordinator.RingEventListener", return_value=f - ): - yield f - - -class _FakeRingListener: - """Test class to replace the ring_doorbell event listener for testing.""" - - def __init__(self, *_, **__) -> None: - self._callbacks = {} - self._subscription_counter = 1 - self.started = False - self.do_not_start = False - - async def start(self, *_, **__): - if self.do_not_start: - return False - self.started = True - return True - - async def stop(self, *_, **__): - self.started = False - - def add_notification_callback(self, callback): - self._callbacks[self._subscription_counter] = callback - self._subscription_counter += 1 - return self._subscription_counter - - def remove_notification_callback(self, subscription_id): - del self._callbacks[subscription_id] - - def notify(self, ring_event: ring_doorbell.RingEvent): - for callback in self._callbacks.values(): - callback(ring_event) + "homeassistant.components.ring.coordinator.RingEventListener", autospec=True + ) as mock_ring_listener: + p = PropertyMock() + p.return_value = True + type(mock_ring_listener.return_value).started = p + yield mock_ring_listener diff --git a/tests/components/ring/device_mocks.py b/tests/components/ring/device_mocks.py index 0a00a9b64deb85..43ebcd10a5c3b0 100644 --- a/tests/components/ring/device_mocks.py +++ b/tests/components/ring/device_mocks.py @@ -29,6 +29,9 @@ DOORBOT_HEALTH = load_json_value_fixture("doorbot_health_attrs.json", DOMAIN) CHIME_HEALTH = load_json_value_fixture("chime_health_attrs.json", DOMAIN) +FRONT_DOOR_DEVICE_ID = 987654 +INGRESS_DEVICE_ID = 185036587 + def get_mock_devices(): """Return list of mock devices keyed by device_type.""" @@ -65,6 +68,7 @@ def get_devices_data(): RingCapability.VOLUME, RingCapability.MOTION_DETECTION, RingCapability.VIDEO, + RingCapability.DING, RingCapability.HISTORY, ], RingStickUpCam: [ @@ -77,7 +81,7 @@ def get_devices_data(): RingCapability.LIGHT, ], RingChime: [RingCapability.VOLUME], - RingOther: [RingCapability.OPEN, RingCapability.HISTORY], + RingOther: [RingCapability.OPEN, RingCapability.HISTORY, RingCapability.DING], } diff --git a/tests/components/ring/test_binary_sensor.py b/tests/components/ring/test_binary_sensor.py index 16bc6e872c1d52..ab1dd630821ca9 100644 --- a/tests/components/ring/test_binary_sensor.py +++ b/tests/components/ring/test_binary_sensor.py @@ -1,24 +1,182 @@ """The tests for the Ring binary sensor platform.""" +import time +from unittest.mock import patch + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.binary_sensor import DOMAIN as BINARY_SENSOR_DOMAIN +from homeassistant.components.ring.binary_sensor import RingEvent +from homeassistant.components.ring.const import DOMAIN +from homeassistant.components.ring.coordinator import RingEventListener from homeassistant.const import Platform from homeassistant.core import HomeAssistant +from homeassistant.helpers import entity_registry as er, issue_registry as ir +from homeassistant.setup import async_setup_component + +from .common import setup_automation, setup_platform +from .device_mocks import FRONT_DOOR_DEVICE_ID, INGRESS_DEVICE_ID + +from tests.common import async_fire_time_changed + + +@pytest.mark.parametrize( + ("device_id", "device_name", "alert_kind", "device_class"), + [ + pytest.param( + FRONT_DOOR_DEVICE_ID, + "front_door", + "motion", + "motion", + id="front_door_motion", + ), + pytest.param( + FRONT_DOOR_DEVICE_ID, + "front_door", + "ding", + "occupancy", + id="front_door_ding", + ), + pytest.param( + INGRESS_DEVICE_ID, "ingress", "ding", "occupancy", id="ingress_ding" + ), + ], +) +async def test_binary_sensor_without_deprecation( + hass: HomeAssistant, + mock_ring_client, + mock_ring_event_listener_class: RingEventListener, + freezer: FrozenDateTimeFactory, + device_id, + device_name, + alert_kind, + device_class, +) -> None: + """Test the Ring binary sensors as if they were not deprecated.""" + with patch( + "homeassistant.components.ring.binary_sensor.async_check_create_deprecated", + return_value=True, + ): + await setup_platform(hass, Platform.BINARY_SENSOR) + + on_event_cb = mock_ring_event_listener_class.return_value.add_notification_callback.call_args.args[ + 0 + ] + + # Default state is set to off + entity_id = f"binary_sensor.{device_name}_{alert_kind}" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "off" + assert state.attributes["device_class"] == device_class + + # A new alert sets to on + event = RingEvent( + 1234546, device_id, "Foo", "Bar", time.time(), 180, kind=alert_kind, state=None + ) + mock_ring_client.active_alerts.return_value = [event] + on_event_cb(event) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "on" + + # Test that another event resets the expiry callback + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + event = RingEvent( + 1234546, device_id, "Foo", "Bar", time.time(), 180, kind=alert_kind, state=None + ) + mock_ring_client.active_alerts.return_value = [event] + on_event_cb(event) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "on" + + freezer.tick(120) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "on" + + # Test the second alert has expired + freezer.tick(60) + async_fire_time_changed(hass) + await hass.async_block_till_done() + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "off" + + +async def test_binary_sensor_not_exists_with_deprecation( + hass: HomeAssistant, + mock_config_entry, + mock_ring_client, + entity_registry: er.EntityRegistry, +) -> None: + """Test the deprecated Ring binary sensors are deleted or raise issues.""" + mock_config_entry.add_to_hass(hass) + + entity_id = "binary_sensor.front_door_motion" + + assert not hass.states.get(entity_id) + with patch("homeassistant.components.ring.PLATFORMS", [Platform.BINARY_SENSOR]): + assert await async_setup_component(hass, DOMAIN, {}) + + assert not entity_registry.async_get(entity_id) + assert not er.async_entries_for_config_entry( + entity_registry, mock_config_entry.entry_id + ) + assert not hass.states.get(entity_id) + -from .common import setup_platform +@pytest.mark.parametrize( + ("entity_disabled", "entity_has_automations"), + [ + pytest.param(False, False, id="without-automations"), + pytest.param(False, True, id="with-automations"), + pytest.param(True, False, id="disabled"), + ], +) +async def test_binary_sensor_exists_with_deprecation( + hass: HomeAssistant, + mock_config_entry, + mock_ring_client, + entity_registry: er.EntityRegistry, + issue_registry: ir.IssueRegistry, + entity_disabled, + entity_has_automations, +) -> None: + """Test the deprecated Ring binary sensors are deleted or raise issues.""" + mock_config_entry.add_to_hass(hass) + entity_id = "binary_sensor.front_door_motion" + unique_id = f"{FRONT_DOOR_DEVICE_ID}-motion" + issue_id = f"deprecated_entity_{entity_id}_automation.test_automation" -async def test_binary_sensor(hass: HomeAssistant, mock_ring_client) -> None: - """Test the Ring binary sensors.""" - await setup_platform(hass, Platform.BINARY_SENSOR) + if entity_has_automations: + await setup_automation(hass, "test_automation", entity_id) - motion_state = hass.states.get("binary_sensor.front_door_motion") - assert motion_state is not None - assert motion_state.state == "on" - assert motion_state.attributes["device_class"] == "motion" + entity = entity_registry.async_get_or_create( + domain=BINARY_SENSOR_DOMAIN, + platform=DOMAIN, + unique_id=unique_id, + suggested_object_id="front_door_motion", + config_entry=mock_config_entry, + disabled_by=er.RegistryEntryDisabler.USER if entity_disabled else None, + ) + assert entity.entity_id == entity_id + assert not hass.states.get(entity_id) + with patch("homeassistant.components.ring.PLATFORMS", [Platform.BINARY_SENSOR]): + assert await async_setup_component(hass, DOMAIN, {}) - front_ding_state = hass.states.get("binary_sensor.front_door_ding") - assert front_ding_state is not None - assert front_ding_state.state == "off" + entity = entity_registry.async_get(entity_id) + # entity and state will be none if removed from registry + assert (entity is None) == entity_disabled + assert (hass.states.get(entity_id) is None) == entity_disabled - ingress_ding_state = hass.states.get("binary_sensor.ingress_ding") - assert ingress_ding_state is not None - assert ingress_ding_state.state == "off" + assert ( + issue_registry.async_get_issue(DOMAIN, issue_id) is not None + ) == entity_has_automations diff --git a/tests/components/ring/test_event.py b/tests/components/ring/test_event.py new file mode 100644 index 00000000000000..88deb0fe5e58e1 --- /dev/null +++ b/tests/components/ring/test_event.py @@ -0,0 +1,79 @@ +"""The tests for the Ring event platform.""" + +from datetime import datetime +import time + +from freezegun.api import FrozenDateTimeFactory +import pytest + +from homeassistant.components.ring.binary_sensor import RingEvent +from homeassistant.components.ring.coordinator import RingEventListener +from homeassistant.const import Platform +from homeassistant.core import HomeAssistant + +from .common import setup_platform +from .device_mocks import FRONT_DOOR_DEVICE_ID, INGRESS_DEVICE_ID + + +@pytest.mark.parametrize( + ("device_id", "device_name", "alert_kind", "device_class"), + [ + pytest.param( + FRONT_DOOR_DEVICE_ID, + "front_door", + "motion", + "motion", + id="front_door_motion", + ), + pytest.param( + FRONT_DOOR_DEVICE_ID, "front_door", "ding", "doorbell", id="front_door_ding" + ), + pytest.param( + INGRESS_DEVICE_ID, "ingress", "ding", "doorbell", id="ingress_ding" + ), + pytest.param( + INGRESS_DEVICE_ID, + "ingress", + "intercom_unlock", + "button", + id="ingress_unlock", + ), + ], +) +async def test_event( + hass: HomeAssistant, + mock_ring_client, + mock_ring_event_listener_class: RingEventListener, + freezer: FrozenDateTimeFactory, + device_id, + device_name, + alert_kind, + device_class, +) -> None: + """Test the Ring event platforms.""" + + await setup_platform(hass, Platform.EVENT) + + start_time_str = "2024-09-04T15:32:53.892+00:00" + start_time = datetime.strptime(start_time_str, "%Y-%m-%dT%H:%M:%S.%f%z") + freezer.move_to(start_time) + on_event_cb = mock_ring_event_listener_class.return_value.add_notification_callback.call_args.args[ + 0 + ] + + # Default state is unknown + entity_id = f"event.{device_name}_{alert_kind}" + state = hass.states.get(entity_id) + assert state is not None + assert state.state == "unknown" + assert state.attributes["device_class"] == device_class + + # A new alert sets to on + event = RingEvent( + 1234546, device_id, "Foo", "Bar", time.time(), 180, kind=alert_kind, state=None + ) + mock_ring_client.active_alerts.return_value = [event] + on_event_cb(event) + state = hass.states.get(entity_id) + assert state is not None + assert state.state == start_time_str diff --git a/tests/components/ring/test_init.py b/tests/components/ring/test_init.py index c3e671c177651e..1119fb997f64c7 100644 --- a/tests/components/ring/test_init.py +++ b/tests/components/ring/test_init.py @@ -1,5 +1,7 @@ """The tests for the Ring component.""" +from unittest.mock import PropertyMock + from freezegun.api import FrozenDateTimeFactory import pytest from ring_doorbell import AuthenticationError, RingError, RingTimeout @@ -8,7 +10,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.light import DOMAIN as LIGHT_DOMAIN from homeassistant.components.ring import DOMAIN -from homeassistant.components.ring.const import SCAN_INTERVAL +from homeassistant.components.ring.const import CONF_LISTEN_CREDENTIALS, SCAN_INTERVAL from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState from homeassistant.const import CONF_TOKEN, CONF_USERNAME from homeassistant.core import HomeAssistant @@ -415,10 +417,35 @@ async def test_token_updated( assert mock_config_entry.data[CONF_TOKEN] == {"access_token": "new-mock-token"} +async def test_listen_token_updated( + hass: HomeAssistant, + freezer: FrozenDateTimeFactory, + mock_config_entry: MockConfigEntry, + mock_ring_client, + mock_ring_event_listener_class, +) -> None: + """Test that the listener token value is updated in the config entry. + + This simulates the api calling the callback. + """ + mock_config_entry.add_to_hass(hass) + + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + + assert mock_ring_event_listener_class.call_count == 1 + token_updater = mock_ring_event_listener_class.call_args.args[2] + + assert mock_config_entry.data.get(CONF_LISTEN_CREDENTIALS) is None + token_updater({"listen_access_token": "mock-token"}) + assert mock_config_entry.data.get(CONF_LISTEN_CREDENTIALS) == { + "listen_access_token": "mock-token" + } + + async def test_no_listen_start( hass: HomeAssistant, caplog: pytest.LogCaptureFixture, - mock_listener, + mock_ring_event_listener_class, mock_ring_client, ) -> None: """Test behaviour if listener doesn't start.""" @@ -427,7 +454,11 @@ async def test_no_listen_start( version=1, data={"username": "foo", "token": {}}, ) - mock_listener.do_not_start = True + mock_ring_event_listener_class.do_not_start = True + + p = PropertyMock() + p.return_value = False + type(mock_ring_event_listener_class.return_value).started = p mock_entry.add_to_hass(hass) await hass.config_entries.async_setup(mock_entry.entry_id) diff --git a/tests/components/ring/test_sensor.py b/tests/components/ring/test_sensor.py index 1f05c120251230..a513fe491b0028 100644 --- a/tests/components/ring/test_sensor.py +++ b/tests/components/ring/test_sensor.py @@ -1,6 +1,7 @@ """The tests for the Ring sensor platform.""" import logging +from unittest.mock import patch from freezegun.api import FrozenDateTimeFactory import pytest @@ -132,7 +133,11 @@ async def test_history_sensor( expected_value, ) -> None: """Test the Ring sensors.""" - await setup_platform(hass, "sensor") + with patch( + "homeassistant.components.ring.sensor.async_check_create_deprecated", + return_value=True, + ): + await setup_platform(hass, "sensor") entity_id = f"sensor.{device_name}_{sensor_name}" sensor_state = hass.states.get(entity_id)