Reolink add 100% coverage of binary_sensor platfrom (#123862)

* Implement 100% coverage of binary_sensor

* fix styling

* Apply suggestions from code review

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>

* use get().state instead of is_state

* Remove unneeded "is True"

* Remove unneeded "is True"

* reset the mock and use assert_not_called

* use freezer

* fix styling

* fix styling

---------

Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
This commit is contained in:
starkillerOG 2024-08-16 12:46:51 +02:00 committed by GitHub
parent 461ef33553
commit 0093276e93
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 170 additions and 40 deletions

View file

@ -53,10 +53,6 @@ def mock_setup_entry() -> Generator[AsyncMock]:
def reolink_connect_class() -> Generator[MagicMock]: def reolink_connect_class() -> Generator[MagicMock]:
"""Mock reolink connection and return both the host_mock and host_mock_class.""" """Mock reolink connection and return both the host_mock and host_mock_class."""
with ( with (
patch(
"homeassistant.components.reolink.host.webhook.async_register",
return_value=True,
),
patch( patch(
"homeassistant.components.reolink.host.Host", autospec=True "homeassistant.components.reolink.host.Host", autospec=True
) as host_mock_class, ) as host_mock_class,

View file

@ -0,0 +1,52 @@
"""Test the Reolink binary sensor platform."""
from unittest.mock import MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
from homeassistant.components.reolink import DEVICE_UPDATE_INTERVAL, const
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import STATE_OFF, STATE_ON, Platform
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from .conftest import TEST_NVR_NAME, TEST_UID
from tests.common import MockConfigEntry, async_fire_time_changed
from tests.typing import ClientSessionGenerator
async def test_motion_sensor(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test binary sensor entity with motion sensor."""
reolink_connect.model = "Reolink Duo PoE"
reolink_connect.motion_detected.return_value = True
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.BINARY_SENSOR]):
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
entity_id = f"{Platform.BINARY_SENSOR}.{TEST_NVR_NAME}_motion_lens_0"
assert hass.states.get(entity_id).state == STATE_ON
reolink_connect.motion_detected.return_value = False
freezer.tick(DEVICE_UPDATE_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done()
assert hass.states.get(entity_id).state == STATE_OFF
# test webhook callback
reolink_connect.motion_detected.return_value = True
reolink_connect.ONVIF_event_callback.return_value = [0]
webhook_id = f"{const.DOMAIN}_{TEST_UID.replace(':', '')}_ONVIF"
client = await hass_client_no_auth()
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
assert hass.states.get(entity_id).state == STATE_ON

View file

@ -1,10 +1,10 @@
"""Test the Reolink config flow.""" """Test the Reolink config flow."""
from datetime import timedelta
import json import json
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock, call from unittest.mock import AsyncMock, MagicMock, call
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError from reolink_aio.exceptions import ApiError, CredentialsInvalidError, ReolinkError
@ -25,7 +25,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.data_entry_flow import FlowResultType from homeassistant.data_entry_flow import FlowResultType
from homeassistant.helpers.device_registry import format_mac from homeassistant.helpers.device_registry import format_mac
from homeassistant.util.dt import utcnow
from .conftest import ( from .conftest import (
DHCP_FORMATTED_MAC, DHCP_FORMATTED_MAC,
@ -439,6 +438,7 @@ async def test_dhcp_flow(hass: HomeAssistant, mock_setup_entry: MagicMock) -> No
) )
async def test_dhcp_ip_update( async def test_dhcp_ip_update(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
reolink_connect_class: MagicMock, reolink_connect_class: MagicMock,
reolink_connect: MagicMock, reolink_connect: MagicMock,
last_update_success: bool, last_update_success: bool,
@ -472,9 +472,8 @@ async def test_dhcp_ip_update(
if not last_update_success: if not last_update_success:
# ensure the last_update_succes is False for the device_coordinator. # ensure the last_update_succes is False for the device_coordinator.
reolink_connect.get_states = AsyncMock(side_effect=ReolinkError("Test error")) reolink_connect.get_states = AsyncMock(side_effect=ReolinkError("Test error"))
async_fire_time_changed( freezer.tick(DEVICE_UPDATE_INTERVAL)
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(minutes=1) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
dhcp_data = dhcp.DhcpServiceInfo( dhcp_data = dhcp.DhcpServiceInfo(

View file

@ -0,0 +1,85 @@
"""Test the Reolink host."""
from asyncio import CancelledError
from unittest.mock import AsyncMock, MagicMock
from aiohttp import ClientResponseError
import pytest
from homeassistant.components.reolink import const
from homeassistant.components.webhook import async_handle_webhook
from homeassistant.config_entries import ConfigEntryState
from homeassistant.core import HomeAssistant
from homeassistant.helpers import entity_registry as er
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.util.aiohttp import MockRequest
from .conftest import TEST_UID
from tests.common import MockConfigEntry
from tests.typing import ClientSessionGenerator
async def test_webhook_callback(
hass: HomeAssistant,
hass_client_no_auth: ClientSessionGenerator,
config_entry: MockConfigEntry,
reolink_connect: MagicMock,
entity_registry: er.EntityRegistry,
) -> None:
"""Test webhook callback with motion sensor."""
assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED
webhook_id = f"{const.DOMAIN}_{TEST_UID.replace(':', '')}_ONVIF"
signal_all = MagicMock()
signal_ch = MagicMock()
async_dispatcher_connect(hass, f"{webhook_id}_all", signal_all)
async_dispatcher_connect(hass, f"{webhook_id}_0", signal_ch)
client = await hass_client_no_auth()
# test webhook callback success all channels
reolink_connect.ONVIF_event_callback.return_value = None
await client.post(f"/api/webhook/{webhook_id}")
signal_all.assert_called_once()
# test webhook callback all channels with failure to read motion_state
signal_all.reset_mock()
reolink_connect.get_motion_state_all_ch.return_value = False
await client.post(f"/api/webhook/{webhook_id}")
signal_all.assert_not_called()
# test webhook callback success single channel
reolink_connect.ONVIF_event_callback.return_value = [0]
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
signal_ch.assert_called_once()
# test webhook callback single channel with error in event callback
signal_ch.reset_mock()
reolink_connect.ONVIF_event_callback = AsyncMock(
side_effect=Exception("Test error")
)
await client.post(f"/api/webhook/{webhook_id}", data="test_data")
signal_ch.assert_not_called()
# test failure to read date from webhook post
request = MockRequest(
method="POST",
content=bytes("test", "utf-8"),
mock_source="test",
)
request.read = AsyncMock(side_effect=ConnectionResetError("Test error"))
await async_handle_webhook(hass, webhook_id, request)
signal_all.assert_not_called()
request.read = AsyncMock(side_effect=ClientResponseError("Test error", "Test"))
await async_handle_webhook(hass, webhook_id, request)
signal_all.assert_not_called()
request.read = AsyncMock(side_effect=CancelledError("Test error"))
with pytest.raises(CancelledError):
await async_handle_webhook(hass, webhook_id, request)
signal_all.assert_not_called()

View file

@ -1,10 +1,10 @@
"""Test the Reolink init.""" """Test the Reolink init."""
import asyncio import asyncio
from datetime import timedelta
from typing import Any from typing import Any
from unittest.mock import AsyncMock, MagicMock, Mock, patch from unittest.mock import AsyncMock, MagicMock, Mock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from reolink_aio.api import Chime from reolink_aio.api import Chime
from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError from reolink_aio.exceptions import CredentialsInvalidError, ReolinkError
@ -25,7 +25,6 @@ from homeassistant.helpers import (
issue_registry as ir, issue_registry as ir,
) )
from homeassistant.setup import async_setup_component from homeassistant.setup import async_setup_component
from homeassistant.util.dt import utcnow
from .conftest import ( from .conftest import (
TEST_CAM_MODEL, TEST_CAM_MODEL,
@ -104,6 +103,7 @@ async def test_failures_parametrized(
async def test_firmware_error_twice( async def test_firmware_error_twice(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
reolink_connect: MagicMock, reolink_connect: MagicMock,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
) -> None: ) -> None:
@ -112,31 +112,31 @@ async def test_firmware_error_twice(
side_effect=ReolinkError("Test error") side_effect=ReolinkError("Test error")
) )
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.UPDATE]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware" entity_id = f"{Platform.UPDATE}.{TEST_NVR_NAME}_firmware"
assert hass.states.is_state(entity_id, STATE_OFF) assert hass.states.get(entity_id).state == STATE_OFF
async_fire_time_changed( freezer.tick(FIRMWARE_UPDATE_INTERVAL)
hass, utcnow() + FIRMWARE_UPDATE_INTERVAL + timedelta(minutes=1) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.is_state(entity_id, STATE_UNAVAILABLE) assert hass.states.get(entity_id).state == STATE_UNAVAILABLE
async def test_credential_error_three( async def test_credential_error_three(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
reolink_connect: MagicMock, reolink_connect: MagicMock,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
issue_registry: ir.IssueRegistry, issue_registry: ir.IssueRegistry,
) -> None: ) -> None:
"""Test when the update gives credential error 3 times.""" """Test when the update gives credential error 3 times."""
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SWITCH]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
@ -147,9 +147,8 @@ async def test_credential_error_three(
issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}" issue_id = f"config_entry_reauth_{const.DOMAIN}_{config_entry.entry_id}"
for _ in range(NUM_CRED_ERRORS): for _ in range(NUM_CRED_ERRORS):
assert (HOMEASSISTANT_DOMAIN, issue_id) not in issue_registry.issues assert (HOMEASSISTANT_DOMAIN, issue_id) not in issue_registry.issues
async_fire_time_changed( freezer.tick(DEVICE_UPDATE_INTERVAL)
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert (HOMEASSISTANT_DOMAIN, issue_id) in issue_registry.issues assert (HOMEASSISTANT_DOMAIN, issue_id) in issue_registry.issues

View file

@ -275,7 +275,7 @@ async def test_browsing_rec_playback_unsupported(
reolink_connect.api_version.return_value = 0 reolink_connect.api_version.return_value = 0
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# browse root # browse root
@ -296,7 +296,7 @@ async def test_browsing_errors(
reolink_connect.api_version.return_value = 1 reolink_connect.api_version.return_value = 1
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
# browse root # browse root
@ -315,7 +315,7 @@ async def test_browsing_not_loaded(
reolink_connect.api_version.return_value = 1 reolink_connect.api_version.return_value = 1
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.CAMERA]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
reolink_connect.get_host_data = AsyncMock(side_effect=ReolinkError("Test error")) reolink_connect.get_host_data = AsyncMock(side_effect=ReolinkError("Test error"))

View file

@ -1,8 +1,8 @@
"""Test the Reolink select platform.""" """Test the Reolink select platform."""
from datetime import timedelta
from unittest.mock import AsyncMock, MagicMock, patch from unittest.mock import AsyncMock, MagicMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest import pytest
from reolink_aio.api import Chime from reolink_aio.api import Chime
from reolink_aio.exceptions import InvalidParameterError, ReolinkError from reolink_aio.exceptions import InvalidParameterError, ReolinkError
@ -19,7 +19,6 @@ from homeassistant.const import (
from homeassistant.core import HomeAssistant from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
from homeassistant.helpers import entity_registry as er from homeassistant.helpers import entity_registry as er
from homeassistant.util.dt import utcnow
from .conftest import TEST_NVR_NAME from .conftest import TEST_NVR_NAME
@ -28,18 +27,19 @@ from tests.common import MockConfigEntry, async_fire_time_changed
async def test_floodlight_mode_select( async def test_floodlight_mode_select(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
reolink_connect: MagicMock, reolink_connect: MagicMock,
entity_registry: er.EntityRegistry, entity_registry: er.EntityRegistry,
) -> None: ) -> None:
"""Test select entity with floodlight_mode.""" """Test select entity with floodlight_mode."""
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_floodlight_mode" entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_floodlight_mode"
assert hass.states.is_state(entity_id, "auto") assert hass.states.get(entity_id).state == "auto"
reolink_connect.set_whiteled = AsyncMock() reolink_connect.set_whiteled = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
@ -71,12 +71,11 @@ async def test_floodlight_mode_select(
) )
reolink_connect.whiteled_mode.return_value = -99 # invalid value reolink_connect.whiteled_mode.return_value = -99 # invalid value
async_fire_time_changed( freezer.tick(DEVICE_UPDATE_INTERVAL)
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.is_state(entity_id, STATE_UNKNOWN) assert hass.states.get(entity_id).state == STATE_UNKNOWN
async def test_play_quick_reply_message( async def test_play_quick_reply_message(
@ -88,12 +87,12 @@ async def test_play_quick_reply_message(
"""Test select play_quick_reply_message entity.""" """Test select play_quick_reply_message entity."""
reolink_connect.quick_reply_dict.return_value = {0: "off", 1: "test message"} reolink_connect.quick_reply_dict.return_value = {0: "off", 1: "test message"}
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_play_quick_reply_message" entity_id = f"{Platform.SELECT}.{TEST_NVR_NAME}_play_quick_reply_message"
assert hass.states.is_state(entity_id, STATE_UNKNOWN) assert hass.states.get(entity_id).state == STATE_UNKNOWN
reolink_connect.play_quick_reply = AsyncMock() reolink_connect.play_quick_reply = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
@ -107,6 +106,7 @@ async def test_play_quick_reply_message(
async def test_chime_select( async def test_chime_select(
hass: HomeAssistant, hass: HomeAssistant,
freezer: FrozenDateTimeFactory,
config_entry: MockConfigEntry, config_entry: MockConfigEntry,
reolink_connect: MagicMock, reolink_connect: MagicMock,
test_chime: Chime, test_chime: Chime,
@ -114,13 +114,13 @@ async def test_chime_select(
) -> None: ) -> None:
"""Test chime select entity.""" """Test chime select entity."""
with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]): with patch("homeassistant.components.reolink.PLATFORMS", [Platform.SELECT]):
assert await hass.config_entries.async_setup(config_entry.entry_id) is True assert await hass.config_entries.async_setup(config_entry.entry_id)
await hass.async_block_till_done() await hass.async_block_till_done()
assert config_entry.state is ConfigEntryState.LOADED assert config_entry.state is ConfigEntryState.LOADED
entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone" entity_id = f"{Platform.SELECT}.test_chime_visitor_ringtone"
assert hass.states.is_state(entity_id, "pianokey") assert hass.states.get(entity_id).state == "pianokey"
test_chime.set_tone = AsyncMock() test_chime.set_tone = AsyncMock()
await hass.services.async_call( await hass.services.async_call(
@ -150,9 +150,8 @@ async def test_chime_select(
) )
test_chime.event_info = {} test_chime.event_info = {}
async_fire_time_changed( freezer.tick(DEVICE_UPDATE_INTERVAL)
hass, utcnow() + DEVICE_UPDATE_INTERVAL + timedelta(seconds=30) async_fire_time_changed(hass)
)
await hass.async_block_till_done() await hass.async_block_till_done()
assert hass.states.is_state(entity_id, STATE_UNKNOWN) assert hass.states.get(entity_id).state == STATE_UNKNOWN