From 669b347ed13d4e5b38a67721127737d7ca8f2737 Mon Sep 17 00:00:00 2001 From: mkmer Date: Mon, 27 Nov 2023 05:13:40 -0500 Subject: [PATCH] Add init test to Blink (#103263) --- .coveragerc | 1 - homeassistant/components/blink/__init__.py | 4 +- homeassistant/components/blink/const.py | 1 + tests/components/blink/conftest.py | 12 +- tests/components/blink/test_init.py | 285 +++++++++++++++++++++ 5 files changed, 295 insertions(+), 8 deletions(-) create mode 100644 tests/components/blink/test_init.py diff --git a/.coveragerc b/.coveragerc index 884afdcf408..1075c0c3e35 100644 --- a/.coveragerc +++ b/.coveragerc @@ -115,7 +115,6 @@ omit = homeassistant/components/beewi_smartclim/sensor.py homeassistant/components/bitcoin/sensor.py homeassistant/components/bizkaibus/sensor.py - homeassistant/components/blink/__init__.py homeassistant/components/blink/alarm_control_panel.py homeassistant/components/blink/binary_sensor.py homeassistant/components/blink/camera.py diff --git a/homeassistant/components/blink/__init__.py b/homeassistant/components/blink/__init__.py index c6413dd4372..7c586a94c3c 100644 --- a/homeassistant/components/blink/__init__.py +++ b/homeassistant/components/blink/__init__.py @@ -158,8 +158,8 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: """Unload Blink entry.""" if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): hass.data[DOMAIN].pop(entry.entry_id) - if not hass.data[DOMAIN]: - return True + if len(hass.data[DOMAIN]) > 0: + return unload_ok hass.services.async_remove(DOMAIN, SERVICE_REFRESH) hass.services.async_remove(DOMAIN, SERVICE_SAVE_VIDEO) diff --git a/homeassistant/components/blink/const.py b/homeassistant/components/blink/const.py index d394b5c0008..64b05e1ba27 100644 --- a/homeassistant/components/blink/const.py +++ b/homeassistant/components/blink/const.py @@ -7,6 +7,7 @@ DEVICE_ID = "Home Assistant" CONF_MIGRATE = "migrate" CONF_CAMERA = "camera" CONF_ALARM_CONTROL_PANEL = "alarm_control_panel" +CONF_DEVICE_ID = "device_id" DEFAULT_BRAND = "Blink" DEFAULT_ATTRIBUTION = "Data provided by immedia-semi.com" DEFAULT_SCAN_INTERVAL = 300 diff --git a/tests/components/blink/conftest.py b/tests/components/blink/conftest.py index 382a1689595..4a731b0a8ee 100644 --- a/tests/components/blink/conftest.py +++ b/tests/components/blink/conftest.py @@ -65,12 +65,14 @@ def blink_api_fixture(camera) -> MagicMock: @pytest.fixture(name="mock_blink_auth_api") -def blink_auth_api_fixture(): +def blink_auth_api_fixture() -> MagicMock: """Set up Blink API fixture.""" - with patch( - "homeassistant.components.blink.Auth", autospec=True - ) as mock_blink_auth_api: - mock_blink_auth_api.check_key_required.return_value = False + mock_blink_auth_api = create_autospec(blinkpy.auth.Auth, instance=True) + mock_blink_auth_api.check_key_required.return_value = False + mock_blink_auth_api.send_auth_key = AsyncMock(return_value=True) + + with patch("homeassistant.components.blink.Auth", autospec=True) as class_mock: + class_mock.return_value = mock_blink_auth_api yield mock_blink_auth_api diff --git a/tests/components/blink/test_init.py b/tests/components/blink/test_init.py new file mode 100644 index 00000000000..76f4a6370e8 --- /dev/null +++ b/tests/components/blink/test_init.py @@ -0,0 +1,285 @@ +"""Test the Blink init.""" +import asyncio +from unittest.mock import AsyncMock, MagicMock, Mock + +from aiohttp import ClientError +import pytest + +from homeassistant.components.blink.const import ( + DOMAIN, + SERVICE_REFRESH, + SERVICE_SAVE_RECENT_CLIPS, + SERVICE_SAVE_VIDEO, + SERVICE_SEND_PIN, +) +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import CONF_FILE_PATH, CONF_FILENAME, CONF_NAME, CONF_PIN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + +CAMERA_NAME = "Camera 1" +FILENAME = "blah" +PIN = "1234" + + +@pytest.mark.parametrize( + ("the_error", "available"), + [(ClientError, False), (asyncio.TimeoutError, False), (None, False)], +) +async def test_setup_not_ready( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, + the_error, + available, +) -> None: + """Test setup failed because we can't connect to the Blink system.""" + + mock_blink_api.start = AsyncMock(side_effect=the_error) + mock_blink_api.available = available + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.SETUP_RETRY + + +async def test_setup_not_ready_authkey_required( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test setup failed because 2FA is needed to connect to the Blink system.""" + + mock_blink_auth_api.check_key_required = MagicMock(return_value=True) + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.SETUP_ERROR + + +async def test_unload_entry( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test being able to unload an entry.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + assert mock_config_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert not hass.services.has_service(DOMAIN, SERVICE_REFRESH) + assert not hass.services.has_service(DOMAIN, SERVICE_SAVE_VIDEO) + assert not hass.services.has_service(DOMAIN, SERVICE_SEND_PIN) + + +async def test_unload_entry_multiple( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test being able to unload one of 2 entries.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + hass.data[DOMAIN]["dummy"] = {1: 2} + assert mock_config_entry.state is ConfigEntryState.LOADED + assert await hass.config_entries.async_unload(mock_config_entry.entry_id) + assert mock_config_entry.state is ConfigEntryState.NOT_LOADED + assert hass.services.has_service(DOMAIN, SERVICE_REFRESH) + assert hass.services.has_service(DOMAIN, SERVICE_SAVE_VIDEO) + assert hass.services.has_service(DOMAIN, SERVICE_SEND_PIN) + + +async def test_migrate_V0( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test migration script version 0.""" + + mock_config_entry.version = 0 + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry.state is ConfigEntryState.LOADED + + +@pytest.mark.parametrize(("version"), [1, 2]) +async def test_migrate( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, + version, +) -> None: + """Test migration scripts.""" + + mock_config_entry.version = version + mock_config_entry.data = {**mock_config_entry.data, "login_response": "Blah"} + + mock_config_entry.add_to_hass(hass) + assert not await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + entry = hass.config_entries.async_get_entry(mock_config_entry.entry_id) + assert entry.state is ConfigEntryState.MIGRATION_ERROR + + +async def test_refresh_service_calls( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test refrest service calls.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_blink_api.refresh.call_count == 1 + + await hass.services.async_call( + DOMAIN, + SERVICE_REFRESH, + blocking=True, + ) + + assert mock_blink_api.refresh.call_count == 2 + + +async def test_video_service_calls( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test video service calls.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_blink_api.refresh.call_count == 1 + + caplog.clear() + await hass.services.async_call( + DOMAIN, + SERVICE_SAVE_VIDEO, + {CONF_NAME: CAMERA_NAME, CONF_FILENAME: FILENAME}, + blocking=True, + ) + assert "no access to path!" in caplog.text + + hass.config.is_allowed_path = Mock(return_value=True) + caplog.clear() + mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} + await hass.services.async_call( + DOMAIN, + SERVICE_SAVE_VIDEO, + {CONF_NAME: CAMERA_NAME, CONF_FILENAME: FILENAME}, + blocking=True, + ) + mock_blink_api.cameras[CAMERA_NAME].video_to_file.assert_awaited_once() + + mock_blink_api.cameras[CAMERA_NAME].video_to_file = AsyncMock(side_effect=OSError) + caplog.clear() + + await hass.services.async_call( + DOMAIN, + SERVICE_SAVE_VIDEO, + {CONF_NAME: CAMERA_NAME, CONF_FILENAME: FILENAME}, + blocking=True, + ) + assert "Can't write image" in caplog.text + + hass.config.is_allowed_path = Mock(return_value=False) + + +async def test_picture_service_calls( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test picture servcie calls.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_blink_api.refresh.call_count == 1 + + caplog.clear() + await hass.services.async_call( + DOMAIN, + SERVICE_SAVE_RECENT_CLIPS, + {CONF_NAME: CAMERA_NAME, CONF_FILE_PATH: FILENAME}, + blocking=True, + ) + assert "no access to path!" in caplog.text + + hass.config.is_allowed_path = Mock(return_value=True) + mock_blink_api.cameras = {CAMERA_NAME: AsyncMock()} + + await hass.services.async_call( + DOMAIN, + SERVICE_SAVE_RECENT_CLIPS, + {CONF_NAME: CAMERA_NAME, CONF_FILE_PATH: FILENAME}, + blocking=True, + ) + mock_blink_api.cameras[CAMERA_NAME].save_recent_clips.assert_awaited_once() + + mock_blink_api.cameras[CAMERA_NAME].save_recent_clips = AsyncMock( + side_effect=OSError + ) + caplog.clear() + + await hass.services.async_call( + DOMAIN, + SERVICE_SAVE_RECENT_CLIPS, + {CONF_NAME: CAMERA_NAME, CONF_FILE_PATH: FILENAME}, + blocking=True, + ) + assert "Can't write recent clips to directory" in caplog.text + + +async def test_pin_service_calls( + hass: HomeAssistant, + mock_blink_api: MagicMock, + mock_blink_auth_api: MagicMock, + mock_config_entry: MockConfigEntry, +) -> None: + """Test pin service calls.""" + + mock_config_entry.add_to_hass(hass) + assert await hass.config_entries.async_setup(mock_config_entry.entry_id) + await hass.async_block_till_done() + + assert mock_config_entry.state is ConfigEntryState.LOADED + assert mock_blink_api.refresh.call_count == 1 + + await hass.services.async_call( + DOMAIN, + SERVICE_SEND_PIN, + {CONF_PIN: PIN}, + blocking=True, + ) + assert mock_blink_api.auth.send_auth_key.assert_awaited_once