From c9d78aa78cd6e3e70b6e8e56e5d4ae7386415dbf Mon Sep 17 00:00:00 2001 From: Jc2k Date: Tue, 25 Feb 2020 11:06:35 +0000 Subject: [PATCH] Refactor homekit_controller config flow tests (#32141) * Config flow test refactor * Add a service and characteristic to the accessory so its more realistic * Feedback from review * Missing apostrophe --- .../homekit_controller/config_flow.py | 31 +- .../homekit_controller/manifest.json | 2 +- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- .../components/homekit_controller/conftest.py | 10 + .../homekit_controller/test_config_flow.py | 883 ++++++------------ 6 files changed, 296 insertions(+), 634 deletions(-) diff --git a/homeassistant/components/homekit_controller/config_flow.py b/homeassistant/components/homekit_controller/config_flow.py index c4101140aaf..4b713636beb 100644 --- a/homeassistant/components/homekit_controller/config_flow.py +++ b/homeassistant/components/homekit_controller/config_flow.py @@ -5,7 +5,6 @@ import os import re import aiohomekit -from aiohomekit import Controller from aiohomekit.controller.ip import IpPairing import voluptuous as vol @@ -59,7 +58,7 @@ def normalize_hkid(hkid): def find_existing_host(hass, serial): """Return a set of the configured hosts.""" for entry in hass.config_entries.async_entries(DOMAIN): - if entry.data["AccessoryPairingID"] == serial: + if entry.data.get("AccessoryPairingID") == serial: return entry @@ -89,7 +88,7 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): self.model = None self.hkid = None self.devices = {} - self.controller = Controller() + self.controller = aiohomekit.Controller() self.finish_pairing = None async def async_step_user(self, user_input=None): @@ -203,6 +202,14 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): _LOGGER.debug("Discovered device %s (%s - %s)", name, model, hkid) + # Device isn't paired with us or anyone else. + # But we have a 'complete' config entry for it - that is probably + # invalid. Remove it automatically. + existing = find_existing_host(self.hass, hkid) + if not paired and existing: + await self.hass.config_entries.async_remove(existing.entry_id) + + # Set unique-id and error out if it's already configured await self.async_set_unique_id(normalize_hkid(hkid)) self._abort_if_unique_id_configured() @@ -230,13 +237,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): if model in HOMEKIT_IGNORE: return self.async_abort(reason="ignored_model") - # Device isn't paired with us or anyone else. - # But we have a 'complete' config entry for it - that is probably - # invalid. Remove it automatically. - existing = find_existing_host(self.hass, hkid) - if existing: - await self.hass.config_entries.async_remove(existing.entry_id) - self.model = model self.hkid = hkid @@ -250,17 +250,6 @@ class HomekitControllerFlowHandler(config_entries.ConfigFlow): hkid = discovery_props["id"] - existing = find_existing_host(self.hass, hkid) - if existing: - _LOGGER.info( - ( - "Legacy configuration for homekit accessory %s" - "not loaded as already migrated" - ), - hkid, - ) - return self.async_abort(reason="already_configured") - _LOGGER.info( ( "Legacy configuration %s for homekit" diff --git a/homeassistant/components/homekit_controller/manifest.json b/homeassistant/components/homekit_controller/manifest.json index 618b6274253..cd2d0c67b44 100644 --- a/homeassistant/components/homekit_controller/manifest.json +++ b/homeassistant/components/homekit_controller/manifest.json @@ -3,7 +3,7 @@ "name": "HomeKit Controller", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/homekit_controller", - "requirements": ["aiohomekit[IP]==0.2.10"], + "requirements": ["aiohomekit[IP]==0.2.11"], "dependencies": [], "zeroconf": ["_hap._tcp.local."], "codeowners": ["@Jc2k"] diff --git a/requirements_all.txt b/requirements_all.txt index 112d8c15cce..edb4564aa72 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -163,7 +163,7 @@ aioftp==0.12.0 aioharmony==0.1.13 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.10 +aiohomekit[IP]==0.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 34745f8e253..15f1a1d82a1 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -62,7 +62,7 @@ aiobotocore==0.11.1 aioesphomeapi==2.6.1 # homeassistant.components.homekit_controller -aiohomekit[IP]==0.2.10 +aiohomekit[IP]==0.2.11 # homeassistant.components.emulated_hue # homeassistant.components.http diff --git a/tests/components/homekit_controller/conftest.py b/tests/components/homekit_controller/conftest.py index cca272be062..99e86335cdb 100644 --- a/tests/components/homekit_controller/conftest.py +++ b/tests/components/homekit_controller/conftest.py @@ -2,6 +2,8 @@ import datetime from unittest import mock +from aiohomekit.testing import FakeController +import asynctest import pytest @@ -12,3 +14,11 @@ def utcnow(request): with mock.patch("homeassistant.util.dt.utcnow") as dt_utcnow: dt_utcnow.return_value = start_dt yield dt_utcnow + + +@pytest.fixture +def controller(hass): + """Replace aiohomekit.Controller with an instance of aiohomekit.testing.FakeController.""" + instance = FakeController() + with asynctest.patch("aiohomekit.Controller", return_value=instance): + yield instance diff --git a/tests/components/homekit_controller/test_config_flow.py b/tests/components/homekit_controller/test_config_flow.py index 144215719dd..e02bc045b3e 100644 --- a/tests/components/homekit_controller/test_config_flow.py +++ b/tests/components/homekit_controller/test_config_flow.py @@ -3,16 +3,17 @@ import json from unittest import mock import aiohomekit +from aiohomekit.model import Accessories, Accessory from aiohomekit.model.characteristics import CharacteristicsTypes from aiohomekit.model.services import ServicesTypes import asynctest +from asynctest import patch import pytest from homeassistant.components.homekit_controller import config_flow -from homeassistant.components.homekit_controller.const import KNOWN_DEVICES from tests.common import MockConfigEntry -from tests.components.homekit_controller.common import Accessory, setup_platform +from tests.components.homekit_controller.common import setup_platform PAIRING_START_FORM_ERRORS = [ (aiohomekit.BusyError, "busy_error"), @@ -79,13 +80,6 @@ def _setup_flow_handler(hass, pairing=None): return flow -async def _setup_flow_zeroconf(hass, discovery_info): - result = await hass.config_entries.flow.async_init( - "homekit_controller", context={"source": "zeroconf"}, data=discovery_info - ) - return result - - @pytest.mark.parametrize("pairing_code", INVALID_PAIRING_CODES) def test_invalid_pairing_codes(pairing_code): """Test ensure_pin_format raises for an invalid pin code.""" @@ -103,288 +97,174 @@ def test_valid_pairing_codes(pairing_code): assert len(valid_pin[2]) == 3 -async def test_discovery_works(hass): - """Test a device being discovered.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - - flow = _setup_flow_handler(hass) - - finish_pairing = asynctest.CoroutineMock() - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery +def get_flow_context(hass, result): + """Get the flow context from the result of async_init or async_configure.""" + flow = next( + ( + flow + for flow in hass.config_entries.flow.async_progress() + if flow["flow_id"] == result["flow_id"] + ) ) + return flow["context"] + + +def get_device_discovery_info(device, upper_case_props=False, missing_csharp=False): + """Turn a aiohomekit format zeroconf entry into a homeassistant one.""" + record = device.info + result = { + "host": record["address"], + "port": record["port"], + "hostname": record["name"], + "type": "_hap._tcp.local.", + "name": record["name"], + "properties": { + "md": record["md"], + "pv": record["pv"], + "id": device.device_id, + "c#": record["c#"], + "s#": record["s#"], + "ff": record["ff"], + "ci": record["ci"], + "sf": 0x01, # record["sf"], + "sh": "", + }, + } + + if missing_csharp: + del result["properties"]["c#"] + + if upper_case_props: + result["properties"] = { + key.upper(): val for (key, val) in result["properties"].items() + } + + return result + + +def setup_mock_accessory(controller): + """Add a bridge accessory to a test controller.""" + bridge = Accessories() + + accessory = Accessory( + name="Koogeek-LS1-20833F", + manufacturer="Koogeek", + model="LS1", + serial_number="12345", + firmware_revision="1.1", + ) + + service = accessory.add_service(ServicesTypes.LIGHTBULB) + on_char = service.add_char(CharacteristicsTypes.ON) + on_char.value = 0 + + bridge.add_accessory(accessory) + + return controller.add_device(bridge) + + +@pytest.mark.parametrize("upper_case_props", [True, False]) +@pytest.mark.parametrize("missing_csharp", [True, False]) +async def test_discovery_works(hass, controller, upper_case_props, missing_csharp): + """Test a device being discovered.""" + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device, upper_case_props, missing_csharp) + # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) assert result["type"] == "form" assert result["step_id"] == "pair" - assert flow.context == { + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", + "source": "zeroconf", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", } # User initiates pairing - device enters pairing mode and displays code - result = await flow.async_step_pair({}) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "pair" - assert discovery.start_pairing.call_count == 1 - - pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) - - pairing.list_accessories_and_characteristics = asynctest.CoroutineMock( - return_value=[ - { - "aid": 1, - "services": [ - { - "characteristics": [ - {"type": "23", "value": "Koogeek-LS1-20833F"} - ], - "type": "3e", - } - ], - } - ] - ) - - finish_pairing.return_value = pairing # Pairing doesn't error error and pairing results - flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" - assert result["data"] == pairing.pairing_data + assert result["data"] == {} -async def test_discovery_works_upper_case(hass): - """Test a device being discovered.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"MD": "TestDevice", "ID": "00:00:00:00:00:00", "C#": 1, "SF": 1}, - } - - flow = _setup_flow_handler(hass) - - finish_pairing = asynctest.CoroutineMock() - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) - - # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } - - # User initiates pairing - device enters pairing mode and displays code - result = await flow.async_step_pair({}) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert discovery.start_pairing.call_count == 1 - - pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) - - pairing.list_accessories_and_characteristics = asynctest.CoroutineMock( - return_value=[ - { - "aid": 1, - "services": [ - { - "characteristics": [ - {"type": "23", "value": "Koogeek-LS1-20833F"} - ], - "type": "3e", - } - ], - } - ] - ) - - finish_pairing.return_value = pairing - - flow.controller.pairings = {"00:00:00:00:00:00": pairing} - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) - assert result["type"] == "create_entry" - assert result["title"] == "Koogeek-LS1-20833F" - assert result["data"] == pairing.pairing_data - - -async def test_discovery_works_missing_csharp(hass): - """Test a device being discovered that has missing mdns attrs.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "sf": 1}, - } - - flow = _setup_flow_handler(hass) - - finish_pairing = asynctest.CoroutineMock() - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) - - # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } - - # User initiates pairing - device enters pairing mode and displays code - result = await flow.async_step_pair({}) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert discovery.start_pairing.call_count == 1 - - pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) - - pairing.list_accessories_and_characteristics = asynctest.CoroutineMock( - return_value=[ - { - "aid": 1, - "services": [ - { - "characteristics": [ - {"type": "23", "value": "Koogeek-LS1-20833F"} - ], - "type": "3e", - } - ], - } - ] - ) - - finish_pairing.return_value = pairing - - flow.controller.pairings = {"00:00:00:00:00:00": pairing} - - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) - assert result["type"] == "create_entry" - assert result["title"] == "Koogeek-LS1-20833F" - assert result["data"] == pairing.pairing_data - - -async def test_abort_duplicate_flow(hass): +async def test_abort_duplicate_flow(hass, controller): """Already paired.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) - result = await _setup_flow_zeroconf(hass, discovery_info) + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) assert result["type"] == "form" assert result["step_id"] == "pair" - result = await _setup_flow_zeroconf(hass, discovery_info) + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) assert result["type"] == "abort" assert result["reason"] == "already_in_progress" -async def test_pair_already_paired_1(hass): +async def test_pair_already_paired_1(hass, controller): """Already paired.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 0}, - } + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) - flow = _setup_flow_handler(hass) + # Flag device as already paired + discovery_info["properties"]["sf"] = 0x0 - result = await flow.async_step_zeroconf(discovery_info) + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) assert result["type"] == "abort" assert result["reason"] == "already_paired" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } -async def test_discovery_ignored_model(hass): +async def test_discovery_ignored_model(hass, controller): """Already paired.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": { - "md": config_flow.HOMEKIT_IGNORE[0], - "id": "00:00:00:00:00:00", - "c#": 1, - "sf": 1, - }, - } + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) + discovery_info["properties"]["md"] = config_flow.HOMEKIT_IGNORE[0] - flow = _setup_flow_handler(hass) - - result = await flow.async_step_zeroconf(discovery_info) + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) assert result["type"] == "abort" assert result["reason"] == "ignored_model" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } -async def test_discovery_invalid_config_entry(hass): - """There is already a config entry for the pairing id but its invalid.""" +async def test_discovery_invalid_config_entry(hass, controller): + """There is already a config entry for the pairing id but it's invalid.""" MockConfigEntry( - domain="homekit_controller", data={"AccessoryPairingID": "00:00:00:00:00:00"} + domain="homekit_controller", + data={"AccessoryPairingID": "00:00:00:00:00:00"}, + unique_id="00:00:00:00:00:00", ).add_to_hass(hass) # We just added a mock config entry so it must be visible in hass assert len(hass.config_entries.async_entries()) == 1 - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) - flow = _setup_flow_handler(hass) - - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) # Discovery of a HKID that is in a pairable state but for which there is # already a config entry - in that case the stale config entry is @@ -392,389 +272,227 @@ async def test_discovery_invalid_config_entry(hass): config_entry_count = len(hass.config_entries.async_entries()) assert config_entry_count == 0 + # And new config flow should continue allowing user to set up a new pairing + assert result["type"] == "form" -async def test_discovery_already_configured(hass): + +async def test_discovery_already_configured(hass, controller): """Already configured.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 0}, - } + MockConfigEntry( + domain="homekit_controller", + data={"AccessoryPairingID": "00:00:00:00:00:00"}, + unique_id="00:00:00:00:00:00", + ).add_to_hass(hass) - await setup_platform(hass) + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) - conn = mock.Mock() - conn.config_num = 1 - hass.data[KNOWN_DEVICES]["00:00:00:00:00:00"] = conn + # Set device as already paired + discovery_info["properties"]["sf"] = 0x00 - flow = _setup_flow_handler(hass) - - result = await flow.async_step_zeroconf(discovery_info) + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) assert result["type"] == "abort" assert result["reason"] == "already_configured" - assert flow.context == {} - - assert conn.async_config_num_changed.call_count == 0 - - -async def test_discovery_already_configured_config_change(hass): - """Already configured.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 2, "sf": 0}, - } - - await setup_platform(hass) - - conn = mock.Mock() - conn.config_num = 1 - hass.data[KNOWN_DEVICES]["00:00:00:00:00:00"] = conn - - flow = _setup_flow_handler(hass) - - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - assert flow.context == {} - - assert conn.async_refresh_entity_map.call_args == mock.call(2) @pytest.mark.parametrize("exception,expected", PAIRING_START_ABORT_ERRORS) -async def test_pair_abort_errors_on_start(hass, exception, expected): +async def test_pair_abort_errors_on_start(hass, controller, exception, expected): """Test various pairing errors.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - flow = _setup_flow_handler(hass) - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(side_effect=exception("error")) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) # User initiates pairing - device refuses to enter pairing mode - result = await flow.async_step_pair({}) + test_exc = exception("error") + with patch.object(device, "start_pairing", side_effect=test_exc): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "abort" assert result["reason"] == expected - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } @pytest.mark.parametrize("exception,expected", PAIRING_START_FORM_ERRORS) -async def test_pair_form_errors_on_start(hass, exception, expected): +async def test_pair_form_errors_on_start(hass, controller, exception, expected): """Test various pairing errors.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - flow = _setup_flow_handler(hass) - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(side_effect=exception("error")) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", } # User initiates pairing - device refuses to enter pairing mode - result = await flow.async_step_pair({}) + test_exc = exception("error") + with patch.object(device, "start_pairing", side_effect=test_exc): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["errors"]["pairing_code"] == expected - assert flow.context == { + + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", } @pytest.mark.parametrize("exception,expected", PAIRING_FINISH_ABORT_ERRORS) -async def test_pair_abort_errors_on_finish(hass, exception, expected): +async def test_pair_abort_errors_on_finish(hass, controller, exception, expected): """Test various pairing errors.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - - flow = _setup_flow_handler(hass) - - finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", } - # User initiates pairing - device enters pairing mode and displays code - result = await flow.async_step_pair({}) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert discovery.start_pairing.call_count == 1 + # User initiates pairing - this triggers the device to show a pairing code + # and then HA to show a pairing form + finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) + with patch.object(device, "start_pairing", return_value=finish_pairing): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - # User submits code - pairing fails but can be retried - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) + assert result["type"] == "form" + assert get_flow_context(hass, result) == { + "hkid": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", + } + + # User enters pairing code + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) assert result["type"] == "abort" assert result["reason"] == expected - assert flow.context == { - "hkid": "00:00:00:00:00:00", - "title_placeholders": {"name": "TestDevice"}, - "unique_id": "00:00:00:00:00:00", - } @pytest.mark.parametrize("exception,expected", PAIRING_FINISH_FORM_ERRORS) -async def test_pair_form_errors_on_finish(hass, exception, expected): +async def test_pair_form_errors_on_finish(hass, controller, exception, expected): """Test various pairing errors.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - - flow = _setup_flow_handler(hass) - - finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) - - discovery = mock.Mock() - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) + device = setup_mock_accessory(controller) + discovery_info = get_device_discovery_info(device) # Device is discovered - result = await flow.async_step_zeroconf(discovery_info) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert flow.context == { + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "zeroconf"}, data=discovery_info + ) + + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", } - # User initiates pairing - device enters pairing mode and displays code - result = await flow.async_step_pair({}) - assert result["type"] == "form" - assert result["step_id"] == "pair" - assert discovery.start_pairing.call_count == 1 + # User initiates pairing - this triggers the device to show a pairing code + # and then HA to show a pairing form + finish_pairing = asynctest.CoroutineMock(side_effect=exception("error")) + with patch.object(device, "start_pairing", return_value=finish_pairing): + result = await hass.config_entries.flow.async_configure(result["flow_id"]) - # User submits code - pairing fails but can be retried - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) + assert result["type"] == "form" + assert get_flow_context(hass, result) == { + "hkid": "00:00:00:00:00:00", + "title_placeholders": {"name": "TestDevice"}, + "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", + } + + # User enters pairing code + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) assert result["type"] == "form" assert result["errors"]["pairing_code"] == expected - assert flow.context == { + + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", + "source": "zeroconf", } -async def test_import_works(hass): - """Test a device being discovered.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - - import_info = {"AccessoryPairingID": "00:00:00:00:00:00"} - - pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) - - pairing.list_accessories_and_characteristics = asynctest.CoroutineMock( - return_value=[ - { - "aid": 1, - "services": [ - { - "characteristics": [ - {"type": "23", "value": "Koogeek-LS1-20833F"} - ], - "type": "3e", - } - ], - } - ] - ) - - flow = _setup_flow_handler(hass) - - pairing_cls_imp = ( - "homeassistant.components.homekit_controller.config_flow.IpPairing" - ) - - with mock.patch(pairing_cls_imp) as pairing_cls: - pairing_cls.return_value = pairing - result = await flow.async_import_legacy_pairing( - discovery_info["properties"], import_info - ) - - assert result["type"] == "create_entry" - assert result["title"] == "Koogeek-LS1-20833F" - assert result["data"] == pairing.pairing_data - - -async def test_import_already_configured(hass): - """Test importing a device from .homekit that is already a ConfigEntry.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "properties": {"md": "TestDevice", "id": "00:00:00:00:00:00", "c#": 1, "sf": 1}, - } - - import_info = {"AccessoryPairingID": "00:00:00:00:00:00"} - - config_entry = MockConfigEntry(domain="homekit_controller", data=import_info) - config_entry.add_to_hass(hass) - - flow = _setup_flow_handler(hass) - - result = await flow.async_import_legacy_pairing( - discovery_info["properties"], import_info - ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" - - -async def test_user_works(hass): +async def test_user_works(hass, controller): """Test user initiated disovers devices.""" - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "md": "TestDevice", - "id": "00:00:00:00:00:00", - "c#": 1, - "sf": 1, - } + setup_mock_accessory(controller) - pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) - pairing.list_accessories_and_characteristics = asynctest.CoroutineMock( - return_value=[ - { - "aid": 1, - "services": [ - { - "characteristics": [ - {"type": "23", "value": "Koogeek-LS1-20833F"} - ], - "type": "3e", - } - ], - } - ] + # Device is discovered + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "user"} ) - flow = _setup_flow_handler(hass) - - finish_pairing = asynctest.CoroutineMock(return_value=pairing) - - discovery = mock.Mock() - discovery.info = discovery_info - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - flow.controller.pairings = {"00:00:00:00:00:00": pairing} - flow.controller.discover_ip = asynctest.CoroutineMock(return_value=[discovery]) - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) - - result = await flow.async_step_user() assert result["type"] == "form" assert result["step_id"] == "user" + assert get_flow_context(hass, result) == { + "source": "user", + } - result = await flow.async_step_user({"device": "TestDevice"}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"device": "TestDevice"} + ) assert result["type"] == "form" assert result["step_id"] == "pair" - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) + assert get_flow_context(hass, result) == { + "source": "user", + "unique_id": "00:00:00:00:00:00", + } + + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" - assert result["data"] == pairing.pairing_data -async def test_user_no_devices(hass): +async def test_user_no_devices(hass, controller): """Test user initiated pairing where no devices discovered.""" - flow = _setup_flow_handler(hass) - - flow.controller.discover_ip = asynctest.CoroutineMock(return_value=[]) - result = await flow.async_step_user() - + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "user"} + ) assert result["type"] == "abort" assert result["reason"] == "no_devices" -async def test_user_no_unpaired_devices(hass): +async def test_user_no_unpaired_devices(hass, controller): """Test user initiated pairing where no unpaired devices discovered.""" - flow = _setup_flow_handler(hass) + device = setup_mock_accessory(controller) - discovery_info = { - "name": "TestDevice", - "host": "127.0.0.1", - "port": 8080, - "md": "TestDevice", - "id": "00:00:00:00:00:00", - "c#": 1, - "sf": 0, - } + # Pair the mock device so that it shows as paired in discovery + finish_pairing = await device.start_pairing(device.device_id) + await finish_pairing(device.pairing_code) - discovery = mock.Mock() - discovery.info = discovery_info - - flow.controller.discover_ip = asynctest.CoroutineMock(return_value=[discovery]) - result = await flow.async_step_user() + # Device discovery is requested + result = await hass.config_entries.flow.async_init( + "homekit_controller", context={"source": "user"} + ) assert result["type"] == "abort" assert result["reason"] == "no_devices" @@ -934,103 +652,48 @@ async def test_parse_overlapping_homekit_json(hass): } -async def test_unignore_works(hass): +async def test_unignore_works(hass, controller): """Test rediscovery triggered disovers work.""" - discovery_info = { - "name": "TestDevice", - "address": "127.0.0.1", - "port": 8080, - "md": "TestDevice", - "pv": "1.0", - "id": "00:00:00:00:00:00", - "c#": 1, - "s#": 1, - "ff": 0, - "ci": 0, - "sf": 1, - } + device = setup_mock_accessory(controller) - pairing = mock.Mock(pairing_data={"AccessoryPairingID": "00:00:00:00:00:00"}) - pairing.list_accessories_and_characteristics = asynctest.CoroutineMock( - return_value=[ - { - "aid": 1, - "services": [ - { - "characteristics": [ - {"type": "23", "value": "Koogeek-LS1-20833F"} - ], - "type": "3e", - } - ], - } - ] + # Device is unignored + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": "unignore"}, + data={"unique_id": device.device_id}, ) - - finish_pairing = asynctest.CoroutineMock() - - discovery = mock.Mock() - discovery.device_id = "00:00:00:00:00:00" - discovery.info = discovery_info - discovery.start_pairing = asynctest.CoroutineMock(return_value=finish_pairing) - - finish_pairing.return_value = pairing - - flow = _setup_flow_handler(hass) - - flow.controller.pairings = {"00:00:00:00:00:00": pairing} - flow.controller.discover_ip = asynctest.CoroutineMock(return_value=[discovery]) - - flow.controller.find_ip_by_device_id = asynctest.CoroutineMock( - return_value=discovery - ) - - result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:00"}) assert result["type"] == "form" assert result["step_id"] == "pair" - assert flow.context == { + assert get_flow_context(hass, result) == { "hkid": "00:00:00:00:00:00", "title_placeholders": {"name": "TestDevice"}, "unique_id": "00:00:00:00:00:00", + "source": "unignore", } # User initiates pairing by clicking on 'configure' - device enters pairing mode and displays code - result = await flow.async_step_pair({}) + result = await hass.config_entries.flow.async_configure(result["flow_id"]) assert result["type"] == "form" assert result["step_id"] == "pair" # Pairing finalized - result = await flow.async_step_pair({"pairing_code": "111-22-333"}) + result = await hass.config_entries.flow.async_configure( + result["flow_id"], user_input={"pairing_code": "111-22-333"} + ) assert result["type"] == "create_entry" assert result["title"] == "Koogeek-LS1-20833F" - assert result["data"] == pairing.pairing_data -async def test_unignore_ignores_missing_devices(hass): +async def test_unignore_ignores_missing_devices(hass, controller): """Test rediscovery triggered disovers handle devices that have gone away.""" - discovery_info = { - "name": "TestDevice", - "address": "127.0.0.1", - "port": 8080, - "md": "TestDevice", - "pv": "1.0", - "id": "00:00:00:00:00:00", - "c#": 1, - "s#": 1, - "ff": 0, - "ci": 0, - "sf": 1, - } + setup_mock_accessory(controller) - discovery = mock.Mock() - discovery.device_id = "00:00:00:00:00:00" - discovery.info = discovery_info + # Device is unignored + result = await hass.config_entries.flow.async_init( + "homekit_controller", + context={"source": "unignore"}, + data={"unique_id": "00:00:00:00:00:01"}, + ) - flow = _setup_flow_handler(hass) - flow.controller.discover_ip = asynctest.CoroutineMock(return_value=[discovery]) - - result = await flow.async_step_unignore({"unique_id": "00:00:00:00:00:01"}) assert result["type"] == "abort" - assert flow.context == { - "unique_id": "00:00:00:00:00:01", - } + assert result["reason"] == "no_devices"