mirror of
https://github.com/home-assistant/core.git
synced 2026-08-28 02:24:46 -05:00
1342 lines
49 KiB
Python
1342 lines
49 KiB
Python
"""Tests for iZone config flow."""
|
|
|
|
import asyncio
|
|
from collections.abc import Generator
|
|
from types import SimpleNamespace
|
|
from unittest.mock import AsyncMock, Mock, patch
|
|
|
|
import pytest
|
|
|
|
from homeassistant import config_entries
|
|
from homeassistant.components.izone import config_flow, discovery as izone_discovery
|
|
from homeassistant.components.izone.const import DOMAIN
|
|
from homeassistant.const import CONF_HOST
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResultType
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from .conftest import (
|
|
async_finish_user_discover,
|
|
async_follow_user_handoff,
|
|
async_load_yaml_exclude,
|
|
create_mock_controller,
|
|
endpoint_from_controller,
|
|
patch_discovered_controllers,
|
|
)
|
|
|
|
from tests.common import MockConfigEntry
|
|
|
|
|
|
def _make_homekit_info(md: str, host: str | None = None) -> SimpleNamespace:
|
|
"""Return a minimal HomeKit discovery info object with attributes."""
|
|
return SimpleNamespace(properties={"md": md}, host=host)
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def mock_izone_timeouts() -> Generator[None]:
|
|
"""Mock iZone discovery waits so tests do not sleep for real scan timeouts."""
|
|
with (
|
|
patch(
|
|
"homeassistant.components.izone.discovery.DISCOVERY_IDLE_SECONDS",
|
|
0.04,
|
|
),
|
|
patch(
|
|
"homeassistant.components.izone.config_flow.USER_SCAN_WAIT_SECONDS",
|
|
0,
|
|
),
|
|
):
|
|
yield
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_user_discovery_success(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test user Search hands off to the shelf confirm and creates an entry."""
|
|
controller = create_mock_controller("000000001", "192.0.2.55")
|
|
with patch_discovered_controllers(controller):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000001"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.55"}
|
|
assert result["result"].unique_id == "000000001"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_user_discovery_default_selects_first_and_leaves_other(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Default dropdown selection hands off to first UID; other stays on shelf."""
|
|
first = create_mock_controller("000000001", "192.0.2.1")
|
|
second = create_mock_controller("000000002", "192.0.2.2")
|
|
with patch_discovered_controllers([first, second]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
result = await async_follow_user_handoff(hass, result)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000001"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.1"}
|
|
assert result["result"].unique_id == "000000001"
|
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
|
|
|
progress = [
|
|
p
|
|
for p in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
if p["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
]
|
|
assert len(progress) == 1
|
|
assert progress[0]["step_id"] == "confirm"
|
|
assert progress[0]["context"]["unique_id"] == "000000002"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_broadcast_skips_already_configured_controller(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Search shelf omits configured controllers and hands off the unconfigured one."""
|
|
configured_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
unconfigured_controller = create_mock_controller("000000002", "192.0.2.2")
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id=configured_controller.device_uid,
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
|
|
with patch_discovered_controllers([configured_controller, unconfigured_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000002"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.2"}
|
|
assert result["result"].unique_id == "000000002"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_user_discovery_skips_yaml_excluded_controllers(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""User Search should not offer controllers excluded by deprecated YAML config."""
|
|
excluded_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
allowed_controller = create_mock_controller("000000002", "192.0.2.2")
|
|
await async_load_yaml_exclude(hass, excluded_controller.device_uid)
|
|
|
|
with patch_discovered_controllers([excluded_controller, allowed_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000002"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.2"}
|
|
assert result["result"].unique_id == "000000002"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_broadcast_multiple_unconfigured_shows_choice(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Search shows a controller choice when multiple shelf flows are present."""
|
|
first_controller = create_mock_controller("000000002", "192.0.2.1")
|
|
second_controller = create_mock_controller("000000001", "192.0.2.2")
|
|
|
|
with patch_discovered_controllers([first_controller, second_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
schema_keys = list(result["data_schema"].schema.keys())
|
|
assert len(schema_keys) == 1
|
|
assert str(schema_keys[0].schema) == config_flow.SELECTED_CONTROLLER_UID
|
|
|
|
# GET re-show (user_input is None) must not submit the default.
|
|
rerender = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
assert rerender["type"] is FlowResultType.FORM
|
|
assert rerender["step_id"] == "select_controller"
|
|
|
|
# Default is lowest UID; hand off and leave the other on the shelf.
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
result = await async_follow_user_handoff(hass, result)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000001"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.2"}
|
|
assert result["result"].unique_id == "000000001"
|
|
|
|
entries = hass.config_entries.async_entries(DOMAIN)
|
|
assert len(entries) == 1
|
|
assert entries[0].unique_id == "000000001"
|
|
|
|
progress = [
|
|
p
|
|
for p in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
if p["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
]
|
|
assert len(progress) == 1
|
|
assert progress[0]["step_id"] == "confirm"
|
|
assert progress[0]["context"]["unique_id"] == "000000002"
|
|
|
|
|
|
async def test_select_controller_rerender_hands_off_when_one_left(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Re-show after the shelf shrinks to one candidate hands off that flow."""
|
|
first_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
second_controller = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
with patch_discovered_controllers([first_controller, second_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
|
|
for progress in hass.config_entries.flow.async_progress_by_handler(DOMAIN):
|
|
if progress["context"].get("unique_id") == "000000002":
|
|
hass.config_entries.flow.async_abort(progress["flow_id"])
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "continue_setup"
|
|
assert result["next_flow"] is not None
|
|
|
|
|
|
async def test_select_controller_rerender_aborts_when_shelf_empty(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Re-show after every shelf flow is gone aborts no_devices_found."""
|
|
first_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
second_controller = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
with patch_discovered_controllers([first_controller, second_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
|
|
user_flow_id = result["flow_id"]
|
|
for progress in hass.config_entries.flow.async_progress_by_handler(DOMAIN):
|
|
if progress["flow_id"] != user_flow_id:
|
|
hass.config_entries.flow.async_abort(progress["flow_id"])
|
|
|
|
result = await hass.config_entries.flow.async_configure(user_flow_id)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_select_controller_hands_off_selected_flow_and_leaves_others(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""A selected shelf flow hands off; non-selected shelf flows remain."""
|
|
first_controller = create_mock_controller("000000002", "192.0.2.1")
|
|
second_controller = create_mock_controller("000000001", "192.0.2.2")
|
|
|
|
with patch_discovered_controllers([first_controller, second_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
result = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"],
|
|
{config_flow.SELECTED_CONTROLLER_UID: "000000002"},
|
|
)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000002"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.1"}
|
|
assert result["result"].unique_id == "000000002"
|
|
assert len(hass.config_entries.async_entries(DOMAIN)) == 1
|
|
|
|
skipped_flows = [
|
|
p
|
|
for p in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
if p["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
]
|
|
assert len(skipped_flows) == 1
|
|
assert skipped_flows[0]["step_id"] == "confirm"
|
|
assert skipped_flows[0]["context"]["unique_id"] == "000000001"
|
|
|
|
|
|
async def test_select_controller_aborts_when_uid_not_on_shelf(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Abort no_devices_found when the submitted UID is unknown to the shelf."""
|
|
first_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
second_controller = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
with patch_discovered_controllers([first_controller, second_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
|
|
flow = hass.config_entries.flow._progress[result["flow_id"]]
|
|
result = await flow.async_step_select_controller(
|
|
{config_flow.SELECTED_CONTROLLER_UID: "000000099"}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("entry_source", "entry_data"),
|
|
[
|
|
pytest.param(
|
|
config_entries.SOURCE_USER,
|
|
{CONF_HOST: "192.0.2.1"},
|
|
id="configured",
|
|
),
|
|
pytest.param(
|
|
config_entries.SOURCE_IGNORE,
|
|
{},
|
|
id="ignored",
|
|
),
|
|
],
|
|
)
|
|
async def test_select_controller_aborts_already_configured_when_uid_left_shelf(
|
|
hass: HomeAssistant,
|
|
entry_source: str,
|
|
entry_data: dict[str, str],
|
|
) -> None:
|
|
"""Abort already_configured when the chosen UID was claimed off the shelf."""
|
|
first_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
second_controller = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
with patch_discovered_controllers([first_controller, second_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
|
|
claimed_uid = "000000001"
|
|
for progress in hass.config_entries.flow.async_progress_by_handler(DOMAIN):
|
|
if progress["context"].get("unique_id") == claimed_uid:
|
|
hass.config_entries.flow.async_abort(progress["flow_id"])
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id=claimed_uid,
|
|
source=entry_source,
|
|
data=entry_data,
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
|
|
flow = hass.config_entries.flow._progress[result["flow_id"]]
|
|
result = await flow.async_step_select_controller(
|
|
{config_flow.SELECTED_CONTROLLER_UID: claimed_uid}
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
async def test_broadcast_aborts_when_all_discovered_are_configured(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Search aborts when every noted controller is already configured."""
|
|
configured_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id=configured_controller.device_uid,
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
|
|
with patch_discovered_controllers(configured_controller):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_user_flow_aborts_when_all_discovered_are_ignored(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Search aborts when every noted controller is ignored (no shelf flow)."""
|
|
ignored_controller = create_mock_controller("000000001", "192.0.2.1")
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id=ignored_controller.device_uid,
|
|
source=config_entries.SOURCE_IGNORE,
|
|
data={},
|
|
).add_to_hass(hass)
|
|
|
|
with patch_discovered_controllers(ignored_controller):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_import_aborts_when_another_izone_flow_in_progress(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test YAML import does not overlap discovery with an active user flow."""
|
|
controller = create_mock_controller("000000001", "192.0.2.1")
|
|
with patch_discovered_controllers(controller):
|
|
user_flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
assert user_flow["type"] is FlowResultType.SHOW_PROGRESS
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
data={},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_in_progress"
|
|
|
|
|
|
async def test_import_starts_discovery_and_aborts_discovery_started(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""YAML import starts shared discovery then aborts so runtime flows take over."""
|
|
with patch(
|
|
"homeassistant.components.izone.discovery.async_ensure_discovery",
|
|
new=AsyncMock(),
|
|
) as mock_ensure:
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
data={},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "discovery_started"
|
|
mock_ensure.assert_awaited_once()
|
|
|
|
|
|
async def test_import_aborts_when_discovery_bind_fails(hass: HomeAssistant) -> None:
|
|
"""YAML import aborts when discovery cannot bind the UDP socket."""
|
|
with patch(
|
|
"homeassistant.components.izone.discovery.async_ensure_discovery",
|
|
new=AsyncMock(side_effect=OSError("bind failed")),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_IMPORT},
|
|
data={},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "discovery_failed"
|
|
|
|
|
|
async def test_user_flow_aborts_when_discovery_bind_fails(hass: HomeAssistant) -> None:
|
|
"""User flow aborts when discovery cannot bind the UDP socket."""
|
|
with patch(
|
|
"homeassistant.components.izone.discovery.async_scan",
|
|
new=AsyncMock(side_effect=OSError("bind failed")),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "discovery_failed"
|
|
|
|
|
|
async def test_user_discover_reshows_progress_while_scan_running(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Re-entering discover while the scan task is pending keeps SHOW_PROGRESS."""
|
|
release = asyncio.Event()
|
|
|
|
async def _blocked_scan(_hass: HomeAssistant) -> None:
|
|
await release.wait()
|
|
|
|
with patch(
|
|
"homeassistant.components.izone.discovery.async_scan",
|
|
new=AsyncMock(side_effect=_blocked_scan),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
|
assert result["progress_action"] == "discover"
|
|
|
|
still_progress = await hass.config_entries.flow.async_configure(
|
|
result["flow_id"]
|
|
)
|
|
assert still_progress["type"] is FlowResultType.SHOW_PROGRESS
|
|
assert still_progress["progress_action"] == "discover"
|
|
|
|
release.set()
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"])
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_user_search_skips_peer_user_flow_when_building_candidates(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Concurrent SOURCE_USER flows are not treated as shelf candidates."""
|
|
controller = create_mock_controller("000000001", "192.0.2.1")
|
|
with patch_discovered_controllers(controller):
|
|
first = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
second = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
assert first["type"] is FlowResultType.SHOW_PROGRESS
|
|
assert second["type"] is FlowResultType.SHOW_PROGRESS
|
|
|
|
first = await async_finish_user_discover(hass, first)
|
|
|
|
assert first["type"] is FlowResultType.ABORT
|
|
assert first["reason"] == "continue_setup"
|
|
assert first["next_flow"] is not None
|
|
assert second["flow_id"] in {
|
|
progress["flow_id"]
|
|
for progress in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
}
|
|
|
|
|
|
async def test_user_candidates_skips_discovery_without_host_placeholder(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Discovery flows missing a string host placeholder are not offered."""
|
|
first = create_mock_controller("000000001", "192.0.2.1")
|
|
second = create_mock_controller("000000002", "192.0.2.2")
|
|
with patch_discovered_controllers([first, second]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
|
|
flow = hass.config_entries.flow._progress[result["flow_id"]]
|
|
incomplete = {
|
|
"flow_id": "incomplete-discovery",
|
|
"handler": DOMAIN,
|
|
"context": {
|
|
"source": config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
|
"unique_id": "000000099",
|
|
},
|
|
"step_id": "confirm",
|
|
}
|
|
progress = list(hass.config_entries.flow.async_progress_by_handler(DOMAIN))
|
|
with patch.object(
|
|
hass.config_entries.flow,
|
|
"async_progress_by_handler",
|
|
return_value=[incomplete, *progress],
|
|
):
|
|
candidates = flow._async_user_candidates()
|
|
|
|
assert {candidate.uid for candidate in candidates} == {"000000001", "000000002"}
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_homekit_confirm_uses_discovered_host(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test HomeKit flow confirms and uses the discovered controller IP, not the HomeKit host."""
|
|
controller = create_mock_controller(device_ip="192.0.2.3")
|
|
|
|
with patch_discovered_controllers(controller):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "confirm"
|
|
flow = next(
|
|
flow
|
|
for flow in hass.config_entries.flow.async_progress()
|
|
if flow["flow_id"] == result["flow_id"]
|
|
)
|
|
assert flow["context"]["title_placeholders"] == {
|
|
"name": "iZone 000000001",
|
|
"host": "192.0.2.3",
|
|
}
|
|
assert result["description_placeholders"] == {
|
|
"controller_uid": "000000001",
|
|
"host": "192.0.2.3",
|
|
}
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000001"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.3"}
|
|
assert result["result"].unique_id == "000000001"
|
|
|
|
|
|
async def test_homekit_fans_out_other_discovered_controllers(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test HomeKit flow fans out additional discovered controllers."""
|
|
matched_controller = create_mock_controller("000000001", "192.0.2.3")
|
|
other_controller = create_mock_controller("000000002", "192.0.2.4")
|
|
|
|
with patch_discovered_controllers([matched_controller, other_controller]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "confirm"
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
progress = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
assert len(progress) == 2
|
|
|
|
current_flow = next(
|
|
flow for flow in progress if flow["flow_id"] == result["flow_id"]
|
|
)
|
|
assert current_flow["context"]["source"] == config_entries.SOURCE_HOMEKIT
|
|
|
|
fanout_flow = next(
|
|
flow for flow in progress if flow["flow_id"] != result["flow_id"]
|
|
)
|
|
assert fanout_flow["step_id"] == "confirm"
|
|
assert (
|
|
fanout_flow["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
)
|
|
assert fanout_flow["context"]["unique_id"] == "000000002"
|
|
|
|
|
|
async def test_homekit_flow_sets_device_uid_once(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""HomeKit flow sets unique_id to the device UID exactly once (no lock-ID swap)."""
|
|
controller = create_mock_controller("000000001", "192.0.2.3")
|
|
set_unique_id_calls: list[str] = []
|
|
original_set_unique_id = config_flow.IZoneConfigFlow.async_set_unique_id
|
|
|
|
async def _recording_set_unique_id(
|
|
self: config_flow.IZoneConfigFlow, uid: str
|
|
) -> None:
|
|
set_unique_id_calls.append(uid)
|
|
await original_set_unique_id(self, uid)
|
|
|
|
with (
|
|
patch_discovered_controllers(controller),
|
|
patch.object(
|
|
config_flow.IZoneConfigFlow,
|
|
"async_set_unique_id",
|
|
_recording_set_unique_id,
|
|
),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "confirm"
|
|
assert set_unique_id_calls == ["000000001"]
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_homekit_aborts_while_user_select_is_open(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""HomeKit onboarding for same UID is blocked while user Search select is open."""
|
|
first = create_mock_controller("000000001", "192.0.2.3")
|
|
second = create_mock_controller("000000002", "192.0.2.4")
|
|
with patch_discovered_controllers([first, second]):
|
|
user_flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
user_flow = await async_finish_user_discover(hass, user_flow)
|
|
assert user_flow["type"] is FlowResultType.FORM
|
|
assert user_flow["step_id"] == "select_controller"
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_in_progress"
|
|
|
|
|
|
async def test_user_search_allowed_while_homekit_flow_in_progress(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""User Search may start while a HomeKit confirm flow is already open."""
|
|
homekit_controller = create_mock_controller("000000001", "192.0.2.3")
|
|
other_controller = create_mock_controller("000000002", "192.0.2.4")
|
|
with patch_discovered_controllers([homekit_controller, other_controller]):
|
|
homekit_flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert homekit_flow["type"] is FlowResultType.FORM
|
|
assert homekit_flow["step_id"] == "confirm"
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_USER},
|
|
)
|
|
assert result["type"] is FlowResultType.SHOW_PROGRESS
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "select_controller"
|
|
|
|
|
|
async def test_homekit_aborts_when_uid_already_configured(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test HomeKit aborts immediately when the discovered UID is already configured."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000001",
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_all_endpoints",
|
|
) as mock_discover_all,
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_endpoint",
|
|
) as mock_discover_one,
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
mock_discover_all.assert_not_called()
|
|
mock_discover_one.assert_not_called()
|
|
|
|
|
|
async def test_homekit_aborts_when_uid_configured_during_discovery(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test HomeKit aborts if the discovered UID gets configured mid-resolution."""
|
|
controller = create_mock_controller("000000001", "192.0.2.3")
|
|
|
|
async def _discover_with_midflight_config(
|
|
hass: HomeAssistant,
|
|
) -> dict[str, object]:
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000001",
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
return {controller.device_uid: endpoint_from_controller(controller)}
|
|
|
|
with patch(
|
|
"homeassistant.components.izone.discovery.async_discover_all_endpoints",
|
|
new=AsyncMock(side_effect=_discover_with_midflight_config),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
@pytest.mark.parametrize(
|
|
("controllers", "homekit_md", "homekit_host"),
|
|
[
|
|
pytest.param([], "iZone 000000001", "192.0.2.3", id="empty_discovery"),
|
|
pytest.param(
|
|
[create_mock_controller("000000003", "192.0.2.33")],
|
|
"iZone 000000001",
|
|
None,
|
|
id="uid_missing_from_discovery",
|
|
),
|
|
pytest.param(
|
|
[create_mock_controller("000000002", "192.0.2.44")],
|
|
"iZone 000000001",
|
|
"203.0.113.1",
|
|
id="different_uid_discovered",
|
|
),
|
|
],
|
|
)
|
|
async def test_homekit_aborts_when_target_uid_not_discovered(
|
|
hass: HomeAssistant,
|
|
controllers: list[Mock],
|
|
homekit_md: str,
|
|
homekit_host: str | None,
|
|
) -> None:
|
|
"""HomeKit aborts when iZone discovery does not yield the advertised UID."""
|
|
with patch_discovered_controllers(controllers):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info(homekit_md, homekit_host),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_homekit_resolves_uid_via_discover_endpoint(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""HomeKit falls back to discover_by_uid when the UID is missing from discover_all."""
|
|
target = create_mock_controller("000000001", "192.0.2.1")
|
|
other = create_mock_controller("000000002", "192.0.2.2")
|
|
target_endpoint = endpoint_from_controller(target)
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_all_endpoints",
|
|
new=AsyncMock(
|
|
return_value={other.device_uid: endpoint_from_controller(other)}
|
|
),
|
|
),
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_endpoint",
|
|
new=AsyncMock(return_value=target_endpoint),
|
|
),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "confirm"
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["data"] == {CONF_HOST: "192.0.2.1"}
|
|
assert result["result"].unique_id == "000000001"
|
|
|
|
|
|
async def test_homekit_aborts_when_discovery_bind_fails(hass: HomeAssistant) -> None:
|
|
"""HomeKit aborts when discovery cannot bind the UDP socket."""
|
|
with patch(
|
|
"homeassistant.components.izone.discovery.async_discover_all_endpoints",
|
|
new=AsyncMock(side_effect=OSError("bind failed")),
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "203.0.113.1"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "discovery_failed"
|
|
|
|
|
|
async def test_user_flow_aborts_when_no_controllers_found(hass: HomeAssistant) -> None:
|
|
"""User flow aborts when broadcast discovery returns no controllers."""
|
|
with patch_discovered_controllers([]):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_homekit_without_model_aborts(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Test HomeKit flow with a non-iZone model string aborts immediately."""
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("Other Device", "192.0.2.3"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_integration_discovery_aborts_for_yaml_excluded_uid(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Integration discovery should abort for UIDs excluded in YAML config."""
|
|
await async_load_yaml_exclude(hass, "000000002")
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
|
"unique_id": "000000002",
|
|
},
|
|
data={CONF_HOST: "192.0.2.2"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
|
|
|
|
async def test_integration_discovery_aborts_for_ignored_uid(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Integration discovery should abort for UIDs that have been ignored."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000002",
|
|
source=config_entries.SOURCE_IGNORE,
|
|
data={},
|
|
).add_to_hass(hass)
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
|
"unique_id": "000000002",
|
|
},
|
|
data={CONF_HOST: "192.0.2.2"},
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_runtime_integration_discovery_starts_confirm_flow(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""When the discovery service sees an unconfigured UID, offer setup."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000001",
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
new_ctrl = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
izone_discovery.async_note_integration_discovery(
|
|
hass, endpoint_from_controller(new_ctrl)
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
progress = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
assert len(progress) == 1
|
|
assert (
|
|
progress[0]["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
)
|
|
assert progress[0]["step_id"] == "confirm"
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_integration_discovery_confirm_creates_entry(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Full path: integration-discovery flow confirmed by the user creates an entry."""
|
|
controller = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={
|
|
"source": config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
|
"unique_id": controller.device_uid,
|
|
},
|
|
data={CONF_HOST: controller.device_ip},
|
|
)
|
|
assert result["type"] is FlowResultType.FORM
|
|
assert result["step_id"] == "confirm"
|
|
|
|
result = await hass.config_entries.flow.async_configure(result["flow_id"], {})
|
|
await hass.async_block_till_done()
|
|
|
|
assert result["type"] is FlowResultType.CREATE_ENTRY
|
|
assert result["title"] == "iZone 000000002"
|
|
assert result["data"] == {CONF_HOST: "192.0.2.2"}
|
|
assert result["result"].unique_id == "000000002"
|
|
|
|
|
|
async def test_runtime_integration_discovery_skips_yaml_excluded_uid(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Deprecated YAML exclude suppresses auto discovery flows."""
|
|
await async_load_yaml_exclude(hass, "000000002")
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000001",
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
excluded_ctrl = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
with patch(
|
|
"homeassistant.helpers.discovery_flow.async_create_flow"
|
|
) as mock_create_flow:
|
|
izone_discovery.async_note_integration_discovery(
|
|
hass, endpoint_from_controller(excluded_ctrl)
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
mock_create_flow.assert_not_called()
|
|
|
|
|
|
async def test_runtime_integration_discovery_skips_when_uid_already_configured(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""No active flow remains when a config entry already exists for the UID."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000002",
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
ctrl = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
izone_discovery.async_note_integration_discovery(
|
|
hass, endpoint_from_controller(ctrl)
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
|
|
|
|
async def test_runtime_integration_discovery_skips_for_ignored_unique_id(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""No active flow remains when the UID matches an ignored entry."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000002",
|
|
source=config_entries.SOURCE_IGNORE,
|
|
data={},
|
|
).add_to_hass(hass)
|
|
ctrl = create_mock_controller("000000002", "192.0.2.2")
|
|
|
|
izone_discovery.async_note_integration_discovery(
|
|
hass, endpoint_from_controller(ctrl)
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
assert not hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
|
|
|
|
async def test_runtime_integration_discovery_allows_during_user_select_controller_step(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Runtime discovery may add shelf flows while the user is choosing controllers."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000001",
|
|
data={},
|
|
version=2,
|
|
).add_to_hass(hass)
|
|
first = create_mock_controller("000000002", "192.0.2.2")
|
|
second = create_mock_controller("000000003", "192.0.2.3")
|
|
with patch_discovered_controllers([first, second]):
|
|
user_flow = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
user_flow = await async_finish_user_discover(hass, user_flow)
|
|
assert user_flow["type"] is FlowResultType.FORM
|
|
assert user_flow["step_id"] == "select_controller"
|
|
|
|
discovery_flows = [
|
|
flow
|
|
for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
if flow["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
]
|
|
assert len(discovery_flows) == 2
|
|
|
|
# Re-noting an existing shelf UID must not stack another flow.
|
|
izone_discovery.async_note_integration_discovery(
|
|
hass, endpoint_from_controller(first)
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
discovery_flows = [
|
|
flow
|
|
for flow in hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
if flow["context"]["source"] == config_entries.SOURCE_INTEGRATION_DISCOVERY
|
|
]
|
|
assert len(discovery_flows) == 2
|
|
assert {flow["context"]["unique_id"] for flow in discovery_flows} == {
|
|
first.device_uid,
|
|
second.device_uid,
|
|
}
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_entry_setup")
|
|
async def test_runtime_integration_discovery_allows_during_user_confirm(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Runtime discovery may add shelf flows while a shelf confirm step is open."""
|
|
first = create_mock_controller("000000001", "192.0.2.1")
|
|
second = create_mock_controller("000000002", "192.0.2.2")
|
|
with patch_discovered_controllers(first):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
assert result["step_id"] == "confirm"
|
|
|
|
izone_discovery.async_note_integration_discovery(
|
|
hass, endpoint_from_controller(second)
|
|
)
|
|
await hass.async_block_till_done(wait_background_tasks=True)
|
|
|
|
progress = hass.config_entries.flow.async_progress_by_handler(DOMAIN)
|
|
assert len(progress) == 2
|
|
sources = {flow["context"]["source"] for flow in progress}
|
|
assert sources == {
|
|
config_entries.SOURCE_INTEGRATION_DISCOVERY,
|
|
}
|
|
|
|
|
|
async def test_async_setup_starts_import_flow(hass: HomeAssistant) -> None:
|
|
"""Test YAML config triggers an import flow."""
|
|
with (
|
|
patch.object(hass.config_entries.flow, "async_init") as mock_async_init,
|
|
patch.object(
|
|
hass,
|
|
"async_create_task",
|
|
side_effect=lambda target: target.close(),
|
|
) as mock_create_task,
|
|
):
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {"exclude": []}})
|
|
|
|
mock_async_init.assert_called_once_with(
|
|
DOMAIN, context={"source": config_entries.SOURCE_IMPORT}
|
|
)
|
|
mock_create_task.assert_called_once()
|
|
|
|
|
|
def test_is_matching_returns_false_when_either_flow_has_no_uid() -> None:
|
|
"""Flow matching should fail when a stable UID cannot be derived."""
|
|
first = SimpleNamespace(context={}, init_data=None)
|
|
second = SimpleNamespace(context={"unique_id": "000000222"}, init_data=None)
|
|
|
|
assert config_flow.IZoneConfigFlow.is_matching(first, second) is False
|
|
|
|
|
|
def test_is_matching_returns_true_for_same_flow_uid() -> None:
|
|
"""Flow matching should succeed when both flows resolve to the same UID."""
|
|
first = SimpleNamespace(context={"unique_id": "000000111"}, init_data=None)
|
|
second = SimpleNamespace(context={"unique_id": "000000111"}, init_data=None)
|
|
|
|
assert config_flow.IZoneConfigFlow.is_matching(first, second) is True
|
|
|
|
|
|
async def test_confirm_asserts_when_controller_data_is_missing(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Confirm asserts when required controller data is unexpectedly missing."""
|
|
controller = create_mock_controller("000000001", "192.0.2.1")
|
|
|
|
with patch_discovered_controllers(controller):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
|
|
# Corrupt flow-local state that the public path always sets before confirm.
|
|
flow = hass.config_entries.flow._progress[result["flow_id"]]
|
|
flow._discovered_controller_ip = None
|
|
with pytest.raises(AssertionError):
|
|
await flow.async_step_confirm()
|
|
|
|
|
|
async def test_confirm_asserts_when_unique_id_is_not_string(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""Confirm asserts when flow unique_id is unexpectedly not a string."""
|
|
controller = create_mock_controller("000000001", "192.0.2.1")
|
|
|
|
with patch_discovered_controllers(controller):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
|
)
|
|
result = await async_finish_user_discover(hass, result)
|
|
result = await async_follow_user_handoff(hass, result)
|
|
|
|
flow = hass.config_entries.flow._progress[result["flow_id"]]
|
|
flow.context["unique_id"] = None
|
|
|
|
with pytest.raises(AssertionError):
|
|
await flow.async_step_confirm({})
|
|
|
|
|
|
def test_async_fan_out_skips_uids_already_in_progress() -> None:
|
|
"""Fan-out skips scheduling flows for UIDs already in progress."""
|
|
candidate = endpoint_from_controller(
|
|
create_mock_controller("000000002", "192.0.2.2")
|
|
)
|
|
# Drive the helper with a stub flow: happy-path fan-out tests only cover the
|
|
# "schedule missing UIDs" branch, not the already-in-progress skip.
|
|
fake_flow = SimpleNamespace(
|
|
_async_current_ids=Mock(return_value=set()),
|
|
_async_in_progress=Mock(return_value=[{"context": {"unique_id": "000000002"}}]),
|
|
_async_schedule_integration_discovery_flow=Mock(),
|
|
)
|
|
|
|
config_flow.IZoneConfigFlow._async_fan_out_discovered_endpoints(
|
|
fake_flow,
|
|
[candidate],
|
|
selected_uid="000000001",
|
|
)
|
|
|
|
fake_flow._async_schedule_integration_discovery_flow.assert_not_called()
|
|
|
|
|
|
async def test_homekit_aborts_for_yaml_excluded_uid_without_discovery(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""HomeKit setup aborts immediately for YAML excluded UIDs."""
|
|
await async_load_yaml_exclude(hass, "000000001")
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_all_endpoints",
|
|
) as mock_discover_all,
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_endpoint",
|
|
) as mock_discover_one,
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "192.0.2.3"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "no_devices_found"
|
|
mock_discover_all.assert_not_called()
|
|
mock_discover_one.assert_not_called()
|
|
|
|
|
|
async def test_homekit_aborts_for_ignored_uid(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""HomeKit setup aborts for UIDs that have been ignored."""
|
|
MockConfigEntry(
|
|
domain=DOMAIN,
|
|
unique_id="000000001",
|
|
source=config_entries.SOURCE_IGNORE,
|
|
data={},
|
|
).add_to_hass(hass)
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_all_endpoints",
|
|
) as mock_discover_all,
|
|
patch(
|
|
"homeassistant.components.izone.discovery.async_discover_endpoint",
|
|
) as mock_discover_one,
|
|
):
|
|
result = await hass.config_entries.flow.async_init(
|
|
DOMAIN,
|
|
context={"source": config_entries.SOURCE_HOMEKIT},
|
|
data=_make_homekit_info("iZone 000000001", "192.0.2.3"),
|
|
)
|
|
|
|
assert result["type"] is FlowResultType.ABORT
|
|
assert result["reason"] == "already_configured"
|
|
mock_discover_all.assert_not_called()
|
|
mock_discover_one.assert_not_called()
|
|
|
|
|
|
async def test_async_migrate_entry_clears_legacy_data(
|
|
hass: HomeAssistant,
|
|
) -> None:
|
|
"""v1→v2 migration clears legacy entry data without network I/O.
|
|
|
|
ConfigEntryNotReady retry semantics only work inside async_setup_entry — raising
|
|
from async_migrate_entry permanently lands the entry in MIGRATION_ERROR with no
|
|
retry path. Setup then heals unique_id=DOMAIN / missing CONF_HOST via discovery.
|
|
"""
|
|
entry = MockConfigEntry(
|
|
domain=DOMAIN,
|
|
version=1,
|
|
unique_id=DOMAIN,
|
|
title="iZone Aircon",
|
|
data={"host": "192.0.2.1"},
|
|
)
|
|
entry.add_to_hass(hass)
|
|
|
|
with patch(
|
|
"homeassistant.components.izone.async_setup_entry",
|
|
return_value=True,
|
|
):
|
|
await hass.config_entries.async_setup(entry.entry_id)
|
|
await hass.async_block_till_done()
|
|
|
|
assert entry.version == 2
|
|
assert entry.data == {}
|