mirror of
https://github.com/home-assistant/core.git
synced 2025-11-10 03:19:34 +00:00
281 lines
10 KiB
Python
281 lines
10 KiB
Python
"""Tests for zeroconf repair issues."""
|
|
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
from zeroconf import ServiceStateChange
|
|
from zeroconf.asyncio import AsyncServiceInfo
|
|
|
|
from homeassistant.components.homeassistant import DOMAIN as HOMEASSISTANT_DOMAIN
|
|
from homeassistant.components.repairs import DOMAIN as REPAIRS_DOMAIN
|
|
from homeassistant.components.zeroconf import DOMAIN, discovery, repairs
|
|
from homeassistant.components.zeroconf.discovery import ZEROCONF_TYPE
|
|
from homeassistant.const import EVENT_HOMEASSISTANT_STARTED
|
|
from homeassistant.core import HomeAssistant
|
|
from homeassistant.data_entry_flow import FlowResultType
|
|
from homeassistant.helpers import instance_id, issue_registry as ir
|
|
from homeassistant.setup import async_setup_component
|
|
|
|
from .test_init import service_update_mock
|
|
|
|
from tests.components.repairs import (
|
|
async_process_repairs_platforms,
|
|
process_repair_fix_flow,
|
|
start_repair_fix_flow,
|
|
)
|
|
from tests.typing import ClientSessionGenerator
|
|
|
|
|
|
def service_state_change_mock(
|
|
zeroconf,
|
|
services,
|
|
handlers,
|
|
*,
|
|
state_change: ServiceStateChange = ServiceStateChange.Removed,
|
|
) -> None:
|
|
"""Call service update handler."""
|
|
for service in services:
|
|
handlers[0](zeroconf, service, f"_name.{service}", state_change)
|
|
|
|
|
|
def _get_hass_service_info_mock(
|
|
service_type: str,
|
|
name: str,
|
|
*,
|
|
instance_id="abc123",
|
|
) -> AsyncServiceInfo:
|
|
"""Return service info for Home Assistant instance."""
|
|
return AsyncServiceInfo(
|
|
ZEROCONF_TYPE,
|
|
name,
|
|
addresses=[b"\n\x00\x00\x01"],
|
|
port=8123,
|
|
weight=0,
|
|
priority=0,
|
|
server="other-host.local.",
|
|
properties={
|
|
"base_url": "http://10.0.0.1:8123",
|
|
"external_url": None,
|
|
"internal_url": "http://10.0.0.1:8123",
|
|
"location_name": "Home",
|
|
"requires_api_password": "True",
|
|
"uuid": instance_id,
|
|
"version": "2025.9.0.dev0",
|
|
},
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
|
async def test_instance_id_conflict_creates_repair_issue_remove(
|
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
|
) -> None:
|
|
"""Test that a repair issue is created on instance ID conflict and gets removed when instance disappears."""
|
|
with (
|
|
patch("homeassistant.helpers.instance_id.async_get", return_value="abc123"),
|
|
patch.object(
|
|
discovery, "AsyncServiceBrowser", side_effect=service_update_mock
|
|
) as mock_browser,
|
|
patch.object(hass.config_entries.flow, "async_init"),
|
|
patch(
|
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
|
side_effect=_get_hass_service_info_mock,
|
|
),
|
|
):
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await hass.async_block_till_done()
|
|
|
|
issue = issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
assert issue
|
|
assert issue.severity == ir.IssueSeverity.ERROR
|
|
assert issue.translation_key == "duplicate_instance_id"
|
|
assert issue.translation_placeholders == {
|
|
"other_host_url": "other-host.local",
|
|
"other_ip": "10.0.0.1",
|
|
"instance_id": "abc123",
|
|
}
|
|
|
|
# Now test that the issue is removed when the service goes away
|
|
service_state_change_mock(
|
|
mock_browser.call_args[0][0],
|
|
[ZEROCONF_TYPE],
|
|
mock_browser.call_args[1]["handlers"],
|
|
)
|
|
assert (
|
|
issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
|
async def test_instance_id_conflict_creates_repair_issue_changing_id(
|
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
|
) -> None:
|
|
"""Test that a repair issue is created on instance ID conflict and gets removed when instance ID changes."""
|
|
with (
|
|
patch("homeassistant.helpers.instance_id.async_get", return_value="abc123"),
|
|
patch.object(
|
|
discovery, "AsyncServiceBrowser", side_effect=service_update_mock
|
|
) as mock_browser,
|
|
patch.object(hass.config_entries.flow, "async_init"),
|
|
patch(
|
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
|
side_effect=_get_hass_service_info_mock,
|
|
),
|
|
):
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await hass.async_block_till_done()
|
|
|
|
issue = issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
assert issue
|
|
assert issue.severity == ir.IssueSeverity.ERROR
|
|
assert issue.translation_key == "duplicate_instance_id"
|
|
assert issue.translation_placeholders == {
|
|
"other_host_url": "other-host.local",
|
|
"other_ip": "10.0.0.1",
|
|
"instance_id": "abc123",
|
|
}
|
|
|
|
with (
|
|
patch(
|
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
|
side_effect=lambda service_type, name: _get_hass_service_info_mock(
|
|
service_type, name, instance_id="different-id"
|
|
),
|
|
),
|
|
):
|
|
# Now test that the issue is removed when the service goes away
|
|
service_state_change_mock(
|
|
mock_browser.call_args[0][0],
|
|
[ZEROCONF_TYPE],
|
|
mock_browser.call_args[1]["handlers"],
|
|
state_change=ServiceStateChange.Updated,
|
|
)
|
|
assert (
|
|
issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
|
async def test_instance_id_no_repair_issue_own_ip(
|
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
|
) -> None:
|
|
"""Test that no repair issue is created when the other instance ID matches our IP."""
|
|
with (
|
|
patch("homeassistant.helpers.instance_id.async_get", return_value="abc123"),
|
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
|
patch.object(hass.config_entries.flow, "async_init"),
|
|
patch(
|
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
|
side_effect=_get_hass_service_info_mock,
|
|
),
|
|
patch(
|
|
"homeassistant.components.network.async_get_announce_addresses",
|
|
return_value=["10.0.0.1", "10.0.0.2"],
|
|
),
|
|
):
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await hass.async_block_till_done()
|
|
|
|
assert (
|
|
issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
|
async def test_instance_id_no_conflict_no_repair_issue(
|
|
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
|
) -> None:
|
|
"""Test that a repair issue is not created when no instance ID conflict exists."""
|
|
with (
|
|
patch("homeassistant.helpers.instance_id.async_get", return_value="xyz123"),
|
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
|
patch.object(hass.config_entries.flow, "async_init"),
|
|
patch(
|
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
|
side_effect=_get_hass_service_info_mock,
|
|
),
|
|
patch("homeassistant.helpers.issue_registry.async_create_issue"),
|
|
):
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await hass.async_block_till_done()
|
|
|
|
assert (
|
|
issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
is None
|
|
)
|
|
|
|
|
|
async def test_create_fix_flow_raises_on_unknown_issue_id(hass: HomeAssistant) -> None:
|
|
"""Test create_fix_flow raises on unknown issue_id."""
|
|
|
|
with pytest.raises(ValueError):
|
|
await repairs.async_create_fix_flow(hass, "no_such_issue", None)
|
|
|
|
|
|
@pytest.mark.usefixtures("mock_async_zeroconf")
|
|
async def test_duplicate_repair_issue_repair_flow(
|
|
hass: HomeAssistant,
|
|
hass_client: ClientSessionGenerator,
|
|
issue_registry: ir.IssueRegistry,
|
|
) -> None:
|
|
"""Test desired flow of the fix flow for duplicate instance ID."""
|
|
assert await async_setup_component(hass, REPAIRS_DOMAIN, {REPAIRS_DOMAIN: {}})
|
|
assert await async_setup_component(hass, HOMEASSISTANT_DOMAIN, {})
|
|
await async_process_repairs_platforms(hass)
|
|
|
|
with (
|
|
patch("homeassistant.helpers.instance_id.async_get", return_value="abc123"),
|
|
patch.object(discovery, "AsyncServiceBrowser", side_effect=service_update_mock),
|
|
patch.object(hass.config_entries.flow, "async_init"),
|
|
patch(
|
|
"homeassistant.components.zeroconf.discovery.AsyncServiceInfo",
|
|
side_effect=_get_hass_service_info_mock,
|
|
),
|
|
patch.object(
|
|
instance_id, "async_recreate", return_value="new-uuid"
|
|
) as mock_recreate,
|
|
patch("homeassistant.config.async_check_ha_config_file", return_value=None),
|
|
patch("homeassistant.core.HomeAssistant.async_stop", return_value=None),
|
|
):
|
|
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
|
|
hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED)
|
|
await hass.async_block_till_done()
|
|
|
|
issue = issue_registry.async_get_issue(
|
|
domain="zeroconf", issue_id="duplicate_instance_id"
|
|
)
|
|
assert issue is not None
|
|
|
|
client = await hass_client()
|
|
|
|
result = await start_repair_fix_flow(client, DOMAIN, issue.issue_id)
|
|
|
|
flow_id = result["flow_id"]
|
|
assert result["type"] == FlowResultType.FORM
|
|
assert result["step_id"] == "confirm_recreate"
|
|
|
|
result = await process_repair_fix_flow(client, flow_id, json={})
|
|
assert result["type"] == "create_entry"
|
|
|
|
await hass.async_block_till_done()
|
|
|
|
assert mock_recreate.called
|