"""Test the thread websocket API.""" import dataclasses from unittest.mock import Mock, patch import pytest from syrupy.assertion import SnapshotAssertion from zeroconf import DNSCache, ServiceInfo from homeassistant.components.thread import dataset_store from homeassistant.components.thread.const import DOMAIN from homeassistant.core import HomeAssistant from homeassistant.setup import async_setup_component from . import DATASET_1 from tests.components.diagnostics import get_diagnostics_for_config_entry from tests.typing import ClientSessionGenerator TEST_ZEROCONF_RECORD_1 = ServiceInfo( type_="_meshcop._udp.local.", name="HomeAssistant OpenThreadBorderRouter #0BBF._meshcop._udp.local.", addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e5"], port=8080, properties={ "rv": "1", "vn": "HomeAssistant", "mn": "OpenThreadBorderRouter", "nn": "OpenThread HC", "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", "tv": "1.3.0", "xa": "\xae\xeb/YKW\x0b\xbf", "sb": "\x00\x00\x01\xb1", "at": "\x00\x00\x00\x00\x00\x01\x00\x00", "pt": "\x8f\x06Q~", "sq": "3", "bb": "\xf0\xbf", "dn": "DefaultDomain", }, ) TEST_ZEROCONF_RECORD_2 = ServiceInfo( type_="_meshcop._udp.local.", name="HomePod._meshcop._udp.local.", addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e4"], port=8080, properties={ "rv": "1", "vn": "Apple", "nn": "OpenThread HC", "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", "tv": "1.2.0", "xa": "\xae\xeb/YKW\x0b\xbf", "sb": "\x00\x00\x01\xb1", "at": "\x00\x00\x00\x00\x00\x01\x00\x00", "pt": "\x8f\x06Q~", "sq": "3", "bb": "\xf0\xbf", "dn": "DefaultDomain", }, ) TEST_ZEROCONF_RECORD_3 = ServiceInfo( type_="_meshcop._udp.local.", name="office._meshcop._udp.local.", addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e0"], port=8080, properties={ "rv": "1", "vn": "Apple", "nn": "OpenThread HC", "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", "tv": "1.2.0", "xa": "\xae\xeb/YKW\x0b\xbf", "sb": "\x00\x00\x01\xb1", "at": "\x00\x00\x00\x00\x00\x01\x00\x00", "pt": "\x8f\x06Q~", "sq": "3", "bb": "\xf0\xbf", "dn": "DefaultDomain", }, ) TEST_ZEROCONF_RECORD_4 = ServiceInfo( type_="_meshcop._udp.local.", name="office._meshcop._udp.local.", addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e0"], port=8080, properties={ "rv": "1", "vn": "Apple", "nn": "OpenThread HC", "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", "tv": "1.2.0", "xa": "\xae\xeb/YKW\x0b\xbf", "sb": "\x00\x00\x01\xb1", "at": "\x00\x00\x00\x00\x00\x01\x00\x00", "pt": "\x8f\x06Q~", "sq": "3", "bb": "\xf0\xbf", "dn": "DefaultDomain", }, ) # Make sure this generates an invalid DNSPointer TEST_ZEROCONF_RECORD_4.name = "office._meshcop._udp.lo\x00cal." # This has no XA TEST_ZEROCONF_RECORD_5 = ServiceInfo( type_="_meshcop._udp.local.", name="bad_1._meshcop._udp.local.", addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e0"], port=8080, properties={ "rv": "1", "vn": "Apple", "nn": "OpenThread HC", "xp": "\xe6\x0f\xc7\xc1\x86!,\xe5", "tv": "1.2.0", "sb": "\x00\x00\x01\xb1", "at": "\x00\x00\x00\x00\x00\x01\x00\x00", "pt": "\x8f\x06Q~", "sq": "3", "bb": "\xf0\xbf", "dn": "DefaultDomain", }, ) # This has no XP TEST_ZEROCONF_RECORD_6 = ServiceInfo( type_="_meshcop._udp.local.", name="bad_2._meshcop._udp.local.", addresses=["127.0.0.1", "fe80::10ed:6406:4ee9:85e0"], port=8080, properties={ "rv": "1", "vn": "Apple", "nn": "OpenThread HC", "tv": "1.2.0", "xa": "\xae\xeb/YKW\x0b\xbf", "sb": "\x00\x00\x01\xb1", "at": "\x00\x00\x00\x00\x00\x01\x00\x00", "pt": "\x8f\x06Q~", "sq": "3", "bb": "\xf0\xbf", "dn": "DefaultDomain", }, ) @dataclasses.dataclass class MockRoute: """A mock iproute2 route table entry.""" dst: str gateway: str | None = None nh_gateway: str | None = None metrics: int = 100 priority: int = 100 family: int = 10 dst_len: int = 64 @dataclasses.dataclass class MockNeighbour: """A mock iproute2 neighbour cache entry.""" dst: str lladdr: str = "00:00:00:00:00:00" state: int = 64 probes: int = 64 @pytest.fixture def ndb() -> Mock: """Prevent NDB poking the OS route tables.""" with patch("pyroute2.NDB") as ndb, ndb() as instance: instance.neighbours = [] instance.routes = [] yield instance async def test_diagnostics( hass: HomeAssistant, mock_async_zeroconf: None, ndb: Mock, hass_client: ClientSessionGenerator, snapshot: SnapshotAssertion, ) -> None: """Test diagnostics for thread routers.""" cache = mock_async_zeroconf.zeroconf.cache = DNSCache() cache.async_add_records( [ *TEST_ZEROCONF_RECORD_1.dns_addresses(), TEST_ZEROCONF_RECORD_1.dns_service(), TEST_ZEROCONF_RECORD_1.dns_text(), TEST_ZEROCONF_RECORD_1.dns_pointer(), ] ) cache.async_add_records( [ *TEST_ZEROCONF_RECORD_2.dns_addresses(), TEST_ZEROCONF_RECORD_2.dns_service(), TEST_ZEROCONF_RECORD_2.dns_text(), TEST_ZEROCONF_RECORD_2.dns_pointer(), ] ) # Test for invalid cache cache.async_add_records([TEST_ZEROCONF_RECORD_3.dns_pointer()]) # Test for invalid record cache.async_add_records( [ *TEST_ZEROCONF_RECORD_4.dns_addresses(), TEST_ZEROCONF_RECORD_4.dns_service(), TEST_ZEROCONF_RECORD_4.dns_text(), TEST_ZEROCONF_RECORD_4.dns_pointer(), ] ) # Test for record without xa cache.async_add_records( [ *TEST_ZEROCONF_RECORD_5.dns_addresses(), TEST_ZEROCONF_RECORD_5.dns_service(), TEST_ZEROCONF_RECORD_5.dns_text(), TEST_ZEROCONF_RECORD_5.dns_pointer(), ] ) # Test for record without xp cache.async_add_records( [ *TEST_ZEROCONF_RECORD_6.dns_addresses(), TEST_ZEROCONF_RECORD_6.dns_service(), TEST_ZEROCONF_RECORD_6.dns_text(), TEST_ZEROCONF_RECORD_6.dns_pointer(), ] ) assert await async_setup_component(hass, DOMAIN, {}) await hass.async_block_till_done() config_entry = hass.config_entries.async_entries(DOMAIN)[0] await dataset_store.async_add_dataset(hass, "source", DATASET_1) ndb.neighbours.append( MockNeighbour( dst="fe80::10ed:6406:4ee9:85e5", ) ) ndb.neighbours.append( MockNeighbour( dst="fe80::10ed:6406:4ee9:85e4", ) ) ndb.routes.append( MockRoute( dst="fd59:86c6:e5a5::", gateway="fe80::10ed:6406:4ee9:85e5", ) ) ndb.routes.append( MockRoute( dst="fd59:86c6:e5a5::", nh_gateway="fe80::10ed:6406:4ee9:85e4", ) ) # Add a "ghost" route - we don't know a border router on 85e3 ndb.routes.append( MockRoute( dst="fd59:86c6:e5a5::", nh_gateway="fe80::10ed:6406:4ee9:85e3", ) ) diag = await get_diagnostics_for_config_entry(hass, hass_client, config_entry) assert diag == snapshot