From a0a5f640dc7218416847f948b047eb8f269e1607 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 18 Jul 2024 15:36:54 -0500 Subject: [PATCH] Add some basic tests for doorbird (#122135) * basic tests * basic tests * basic tests * basic tests * cover * cover * Update tests/components/doorbird/test_init.py --- tests/components/doorbird/__init__.py | 39 +++++++ tests/components/doorbird/conftest.py | 101 ++++++++++++++++++ tests/components/doorbird/fixtures/info.json | 23 ++++ .../doorbird/fixtures/schedule.json | 67 ++++++++++++ tests/components/doorbird/test_config_flow.py | 100 ++++++----------- tests/components/doorbird/test_init.py | 35 ++++++ 6 files changed, 297 insertions(+), 68 deletions(-) create mode 100644 tests/components/doorbird/conftest.py create mode 100644 tests/components/doorbird/fixtures/info.json create mode 100644 tests/components/doorbird/fixtures/schedule.json create mode 100644 tests/components/doorbird/test_init.py diff --git a/tests/components/doorbird/__init__.py b/tests/components/doorbird/__init__.py index 57bf4c04e39..20f0b0262d5 100644 --- a/tests/components/doorbird/__init__.py +++ b/tests/components/doorbird/__init__.py @@ -1 +1,40 @@ """Tests for the DoorBird integration.""" + +from typing import Any +from unittest.mock import AsyncMock, MagicMock, Mock + +import aiohttp +from doorbirdpy import DoorBird, DoorBirdScheduleEntry + +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNAME + +VALID_CONFIG = { + CONF_HOST: "1.2.3.4", + CONF_USERNAME: "friend", + CONF_PASSWORD: "password", + CONF_NAME: "mydoorbird", +} + + +def mock_unauthorized_exception() -> aiohttp.ClientResponseError: + """Return a mock unauthorized exception.""" + return aiohttp.ClientResponseError(request_info=Mock(), history=Mock(), status=401) + + +def get_mock_doorbird_api( + info: dict[str, Any] | None = None, + info_side_effect: Exception | None = None, + schedule: list[DoorBirdScheduleEntry] | None = None, +) -> DoorBird: + """Return a mock DoorBirdAPI object with return values.""" + doorbirdapi_mock = MagicMock(spec_set=DoorBird) + type(doorbirdapi_mock).info = AsyncMock( + side_effect=info_side_effect, return_value=info + ) + type(doorbirdapi_mock).favorites = AsyncMock(return_value={}) + type(doorbirdapi_mock).change_favorite = AsyncMock(return_value=True) + type(doorbirdapi_mock).schedule = AsyncMock(return_value=schedule) + type(doorbirdapi_mock).doorbell_state = AsyncMock( + side_effect=mock_unauthorized_exception() + ) + return doorbirdapi_mock diff --git a/tests/components/doorbird/conftest.py b/tests/components/doorbird/conftest.py new file mode 100644 index 00000000000..bb623e69485 --- /dev/null +++ b/tests/components/doorbird/conftest.py @@ -0,0 +1,101 @@ +"""Test configuration for DoorBird tests.""" + +from collections.abc import Generator +from contextlib import contextmanager +from dataclasses import dataclass +from typing import Any +from unittest.mock import MagicMock, patch + +from doorbirdpy import DoorBird, DoorBirdScheduleEntry +import pytest + +from homeassistant.components.doorbird.const import CONF_EVENTS, DOMAIN +from homeassistant.core import HomeAssistant + +from . import VALID_CONFIG, get_mock_doorbird_api + +from tests.common import MockConfigEntry, load_json_value_fixture + + +@dataclass +class MockDoorbirdEntry: + """Mock DoorBird config entry.""" + + entry: MockConfigEntry + api: MagicMock + + +@pytest.fixture(scope="session") +def doorbird_info() -> dict[str, Any]: + """Return a loaded DoorBird info fixture.""" + return load_json_value_fixture("info.json", "doorbird")["BHA"]["VERSION"][0] + + +@pytest.fixture(scope="session") +def doorbird_schedule() -> list[DoorBirdScheduleEntry]: + """Return a loaded DoorBird schedule fixture.""" + return DoorBirdScheduleEntry.parse_all( + load_json_value_fixture("schedule.json", "doorbird") + ) + + +@pytest.fixture +def doorbird_api( + doorbird_info: dict[str, Any], doorbird_schedule: dict[str, Any] +) -> Generator[Any, Any, DoorBird]: + """Mock the DoorBirdAPI.""" + api = get_mock_doorbird_api(info=doorbird_info, schedule=doorbird_schedule) + with patch_doorbird_api_entry_points(api): + yield api + + +@contextmanager +def patch_doorbird_api_entry_points(api: MagicMock) -> Generator[Any, Any, DoorBird]: + """Mock the DoorBirdAPI.""" + with ( + patch( + "homeassistant.components.doorbird.DoorBird", + return_value=api, + ), + patch( + "homeassistant.components.doorbird.config_flow.DoorBird", + return_value=api, + ), + ): + yield api + + +@pytest.fixture +async def doorbird_mocker( + hass: HomeAssistant, + doorbird_info: dict[str, Any], + doorbird_schedule: dict[str, Any], +) -> MockDoorbirdEntry: + """Create a MockDoorbirdEntry.""" + + async def _async_mock( + entry: MockConfigEntry | None = None, + api: DoorBird | None = None, + info: dict[str, Any] | None = None, + info_side_effect: Exception | None = None, + schedule: list[DoorBirdScheduleEntry] | None = None, + ) -> None: + """Create a MockDoorbirdEntry from defaults or specific values.""" + entry = entry or MockConfigEntry( + domain=DOMAIN, + unique_id="1CCAE3AAAAAA", + data=VALID_CONFIG, + options={CONF_EVENTS: ["event1", "event2", "event3"]}, + ) + api = api or get_mock_doorbird_api( + info=info or doorbird_info, + info_side_effect=info_side_effect, + schedule=schedule or doorbird_schedule, + ) + entry.add_to_hass(hass) + with patch_doorbird_api_entry_points(api): + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + return MockDoorbirdEntry(entry=entry, api=api) + + return _async_mock diff --git a/tests/components/doorbird/fixtures/info.json b/tests/components/doorbird/fixtures/info.json new file mode 100644 index 00000000000..46fb8fbac86 --- /dev/null +++ b/tests/components/doorbird/fixtures/info.json @@ -0,0 +1,23 @@ +{ + "BHA": { + "RETURNCODE": "1", + "VERSION": [ + { + "FIRMWARE": "000125", + "BUILD_NUMBER": "15870439", + "WIFI_MAC_ADDR": "1234ABCD", + "RELAYS": [ + "1", + "2", + "ghchdi@1", + "ghchdi@2", + "ghchdi@3", + "ghdwkh@1", + "ghdwkh@2", + "ghdwkh@3" + ], + "DEVICE-TYPE": "DoorBird D2101V" + } + ] + } +} diff --git a/tests/components/doorbird/fixtures/schedule.json b/tests/components/doorbird/fixtures/schedule.json new file mode 100644 index 00000000000..c300180777c --- /dev/null +++ b/tests/components/doorbird/fixtures/schedule.json @@ -0,0 +1,67 @@ +[ + { + "input": "doorbell", + "param": "1", + "output": [ + { + "event": "notify", + "param": "", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + }, + { + "event": "http", + "param": "0", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + } + ] + }, + { + "input": "motion", + "param": "", + "output": [ + { + "event": "notify", + "param": "", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + }, + { + "event": "http", + "param": "5", + "schedule": { + "weekdays": [ + { + "to": "107999", + "from": "108000" + } + ] + } + } + ] + }, + { + "input": "relay", + "param": "1", + "output": [] + } +] diff --git a/tests/components/doorbird/test_config_flow.py b/tests/components/doorbird/test_config_flow.py index 17cfa05b49e..02b1209af98 100644 --- a/tests/components/doorbird/test_config_flow.py +++ b/tests/components/doorbird/test_config_flow.py @@ -1,9 +1,10 @@ """Test the DoorBird config flow.""" from ipaddress import ip_address -from unittest.mock import AsyncMock, MagicMock, Mock, patch +from unittest.mock import AsyncMock, Mock, patch import aiohttp +from doorbirdpy import DoorBird import pytest from homeassistant import config_entries @@ -18,35 +19,12 @@ from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PASSWORD, CONF_USERNA from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType +from . import VALID_CONFIG, get_mock_doorbird_api, mock_unauthorized_exception + from tests.common import MockConfigEntry -VALID_CONFIG = { - CONF_HOST: "1.2.3.4", - CONF_USERNAME: "friend", - CONF_PASSWORD: "password", - CONF_NAME: "mydoorbird", -} - -def _get_mock_doorbirdapi_return_values(info=None): - doorbirdapi_mock = MagicMock() - type(doorbirdapi_mock).info = AsyncMock(return_value=info) - type(doorbirdapi_mock).doorbell_state = AsyncMock( - side_effect=aiohttp.ClientResponseError( - request_info=Mock(), history=Mock(), status=401 - ) - ) - return doorbirdapi_mock - - -def _get_mock_doorbirdapi_side_effects(info=None): - doorbirdapi_mock = MagicMock() - type(doorbirdapi_mock).info = AsyncMock(side_effect=info) - - return doorbirdapi_mock - - -async def test_user_form(hass: HomeAssistant) -> None: +async def test_user_form(hass: HomeAssistant, doorbird_api: DoorBird) -> None: """Test we get the user form.""" result = await hass.config_entries.flow.async_init( @@ -55,12 +33,7 @@ async def test_user_form(hass: HomeAssistant) -> None: assert result["type"] is FlowResultType.FORM assert result["errors"] == {} - doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"}) with ( - patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch( "homeassistant.components.doorbird.async_setup", return_value=True ) as mock_setup, @@ -178,37 +151,30 @@ async def test_form_zeroconf_non_ipv4_ignored(hass: HomeAssistant) -> None: assert result["reason"] == "not_ipv4_address" -async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: +async def test_form_zeroconf_correct_oui( + hass: HomeAssistant, doorbird_api: DoorBird +) -> None: """Test we can setup from zeroconf with the correct OUI source.""" - doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"}) - with patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ): - result = await hass.config_entries.flow.async_init( - DOMAIN, - context={"source": config_entries.SOURCE_ZEROCONF}, - data=zeroconf.ZeroconfServiceInfo( - ip_address=ip_address("192.168.1.5"), - ip_addresses=[ip_address("192.168.1.5")], - hostname="mock_hostname", - name="Doorstation - abc123._axis-video._tcp.local.", - port=None, - properties={"macaddress": "1CCAE3DOORBIRD"}, - type="mock_type", - ), - ) - await hass.async_block_till_done() + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_ZEROCONF}, + data=zeroconf.ZeroconfServiceInfo( + ip_address=ip_address("192.168.1.5"), + ip_addresses=[ip_address("192.168.1.5")], + hostname="mock_hostname", + name="Doorstation - abc123._axis-video._tcp.local.", + port=None, + properties={"macaddress": "1CCAE3DOORBIRD"}, + type="mock_type", + ), + ) + await hass.async_block_till_done() assert result["type"] is FlowResultType.FORM assert result["step_id"] == "user" assert result["errors"] == {} with ( - patch( - "homeassistant.components.doorbird.config_flow.DoorBird", - return_value=doorbirdapi, - ), patch("homeassistant.components.logbook.async_setup", return_value=True), patch( "homeassistant.components.doorbird.async_setup", return_value=True @@ -244,10 +210,12 @@ async def test_form_zeroconf_correct_oui(hass: HomeAssistant) -> None: ], ) async def test_form_zeroconf_correct_oui_wrong_device( - hass: HomeAssistant, doorbell_state_side_effect + hass: HomeAssistant, + doorbird_api: DoorBird, + doorbell_state_side_effect: Exception | None, ) -> None: """Test we can setup from zeroconf with the correct OUI source but not a doorstation.""" - doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"}) + doorbirdapi = get_mock_doorbird_api(info={"WIFI_MAC_ADDR": "macaddr"}) type(doorbirdapi).doorbell_state = AsyncMock(side_effect=doorbell_state_side_effect) with patch( @@ -278,7 +246,7 @@ async def test_form_user_cannot_connect(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - doorbirdapi = _get_mock_doorbirdapi_side_effects(info=OSError) + doorbirdapi = get_mock_doorbird_api(info_side_effect=OSError) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -298,10 +266,8 @@ async def test_form_user_invalid_auth(hass: HomeAssistant) -> None: DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mock_error = aiohttp.ClientResponseError( - request_info=Mock(), history=Mock(), status=401 - ) - doorbirdapi = _get_mock_doorbirdapi_side_effects(info=mock_error) + mock_error = mock_unauthorized_exception() + doorbirdapi = get_mock_doorbird_api(info_side_effect=mock_error) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -360,10 +326,8 @@ async def test_reauth(hass: HomeAssistant) -> None: assert len(flows) == 1 flow = flows[0] - mock_error = aiohttp.ClientResponseError( - request_info=Mock(), history=Mock(), status=401 - ) - doorbirdapi = _get_mock_doorbirdapi_side_effects(info=mock_error) + mock_error = mock_unauthorized_exception() + doorbirdapi = get_mock_doorbird_api(info_side_effect=mock_error) with patch( "homeassistant.components.doorbird.config_flow.DoorBird", return_value=doorbirdapi, @@ -379,7 +343,7 @@ async def test_reauth(hass: HomeAssistant) -> None: assert result2["type"] is FlowResultType.FORM assert result2["errors"] == {"base": "invalid_auth"} - doorbirdapi = _get_mock_doorbirdapi_return_values(info={"WIFI_MAC_ADDR": "macaddr"}) + doorbirdapi = get_mock_doorbird_api(info={"WIFI_MAC_ADDR": "macaddr"}) with ( patch( "homeassistant.components.doorbird.config_flow.DoorBird", diff --git a/tests/components/doorbird/test_init.py b/tests/components/doorbird/test_init.py new file mode 100644 index 00000000000..5263a1db389 --- /dev/null +++ b/tests/components/doorbird/test_init.py @@ -0,0 +1,35 @@ +"""Test DoorBird init.""" + +from collections.abc import Callable, Coroutine +from typing import Any + +from homeassistant.components.doorbird.const import DOMAIN +from homeassistant.config_entries import ConfigEntryState +from homeassistant.core import HomeAssistant + +from . import mock_unauthorized_exception +from .conftest import MockDoorbirdEntry + + +async def test_basic_setup( + doorbird_mocker: Callable[[], Coroutine[Any, Any, MockDoorbirdEntry]], +) -> None: + """Test basic setup.""" + doorbird_entry = await doorbird_mocker() + entry = doorbird_entry.entry + assert entry.state is ConfigEntryState.LOADED + + +async def test_auth_fails( + hass: HomeAssistant, + doorbird_mocker: Callable[[], Coroutine[Any, Any, MockDoorbirdEntry]], +) -> None: + """Test basic setup with an auth failure.""" + doorbird_entry = await doorbird_mocker( + info_side_effect=mock_unauthorized_exception() + ) + entry = doorbird_entry.entry + assert entry.state is ConfigEntryState.SETUP_ERROR + flows = hass.config_entries.flow.async_progress(DOMAIN) + assert len(flows) == 1 + assert flows[0]["step_id"] == "reauth_confirm"