Skip to content

Commit

Permalink
Add tests
Browse files Browse the repository at this point in the history
  • Loading branch information
sdb9696 committed Sep 4, 2024
1 parent d61f405 commit 3eb6716
Show file tree
Hide file tree
Showing 10 changed files with 335 additions and 64 deletions.
6 changes: 3 additions & 3 deletions homeassistant/components/ring/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 9 additions & 1 deletion homeassistant/components/ring/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,14 @@
}
},
"entity": {
"binary_sensor": {
"ding": {
"name": "Ding"
},
"motion": {
"name": "Motion"
}
},
"event": {
"ding": {
"name": "Ding"
Expand All @@ -40,7 +48,7 @@
"name": "Motion"
},
"intercom_unlock": {
"name": "Unlock"
"name": "Intercom unlock"
}
},
"button": {
Expand Down
16 changes: 16 additions & 0 deletions tests/components/ring/common.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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": {}},
}
},
)
47 changes: 9 additions & 38 deletions tests/components/ring/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
6 changes: 5 additions & 1 deletion tests/components/ring/device_mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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."""
Expand Down Expand Up @@ -65,6 +68,7 @@ def get_devices_data():
RingCapability.VOLUME,
RingCapability.MOTION_DETECTION,
RingCapability.VIDEO,
RingCapability.DING,
RingCapability.HISTORY,
],
RingStickUpCam: [
Expand All @@ -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],
}


Expand Down
186 changes: 172 additions & 14 deletions tests/components/ring/test_binary_sensor.py
Original file line number Diff line number Diff line change
@@ -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
Loading

0 comments on commit 3eb6716

Please sign in to comment.