core/tests/components/zeroconf/test_websocket_api.py
J. Nick Koston 4e7d396e5b
Add WebSocket API to zeroconf to observe discovery (#143540)
* Add WebSocket API to zeroconf to observe discovery

* Add WebSocket API to zeroconf to observe discovery

* increase timeout

* cover

* cover

* cover

* cover

* cover

* cover

* fix lasting side effects

* cleanup merge

* format
2025-04-25 21:18:09 -04:00

195 lines
5.8 KiB
Python

"""The tests for the zeroconf WebSocket API."""
import asyncio
import socket
from unittest.mock import patch
from zeroconf import (
DNSAddress,
DNSPointer,
DNSService,
DNSText,
RecordUpdate,
const,
current_time_millis,
)
from homeassistant.components.zeroconf import DOMAIN, async_get_async_instance
from homeassistant.core import HomeAssistant
from homeassistant.generated import zeroconf as zc_gen
from homeassistant.setup import async_setup_component
from tests.typing import WebSocketGenerator
async def test_subscribe_discovery(
hass: HomeAssistant,
hass_ws_client: WebSocketGenerator,
) -> None:
"""Test zeroconf subscribe_discovery."""
instance = await async_get_async_instance(hass)
instance.zeroconf.cache.async_add_records(
[
DNSPointer(
"_fakeservice._tcp.local.",
const._TYPE_PTR,
const._CLASS_IN,
const._DNS_OTHER_TTL,
"wrong._wrongservice._tcp.local.",
),
DNSPointer(
"_fakeservice._tcp.local.",
const._TYPE_PTR,
const._CLASS_IN,
const._DNS_OTHER_TTL,
"foo2._fakeservice._tcp.local.",
),
DNSService(
"foo2._fakeservice._tcp.local.",
const._TYPE_SRV,
const._CLASS_IN,
const._DNS_OTHER_TTL,
0,
0,
1234,
"foo2.local.",
),
DNSAddress(
"foo2.local.",
const._TYPE_A,
const._CLASS_IN,
const._DNS_HOST_TTL,
socket.inet_aton("127.0.0.1"),
),
DNSText(
"foo2.local.",
const._TYPE_TXT,
const._CLASS_IN,
const._DNS_HOST_TTL,
b"\x13md=HASS Bridge W9DN\x06pv=1.0\x14id=11:8E:DB:5B:5C:C5"
b"\x05c#=12\x04s#=1",
),
DNSPointer(
"_fakeservice._tcp.local.",
const._TYPE_PTR,
const._CLASS_IN,
const._DNS_OTHER_TTL,
"foo3._fakeservice._tcp.local.",
),
DNSService(
"foo3._fakeservice._tcp.local.",
const._TYPE_SRV,
const._CLASS_IN,
const._DNS_OTHER_TTL,
0,
0,
1234,
"foo3.local.",
),
DNSText(
"foo3.local.",
const._TYPE_TXT,
const._CLASS_IN,
const._DNS_HOST_TTL,
b"\x13md=HASS Bridge W9DN\x06pv=1.0\x14id=11:8E:DB:5B:5C:C5"
b"\x05c#=12\x04s#=1",
),
]
)
with patch.dict(
zc_gen.ZEROCONF,
{"_fakeservice._tcp.local.": []},
clear=True,
):
assert await async_setup_component(hass, DOMAIN, {DOMAIN: {}})
await hass.async_block_till_done()
client = await hass_ws_client()
await client.send_json(
{
"id": 1,
"type": "zeroconf/subscribe_discovery",
}
)
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["success"]
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"ip_addresses": ["127.0.0.1"],
"name": "foo2._fakeservice._tcp.local.",
"port": 1234,
"properties": {},
"type": "_fakeservice._tcp.local.",
}
]
}
# now late inject the address record
records = [
DNSAddress(
"foo3.local.",
const._TYPE_A,
const._CLASS_IN,
const._DNS_HOST_TTL,
socket.inet_aton("127.0.0.1"),
),
]
instance.zeroconf.cache.async_add_records(records)
instance.zeroconf.record_manager.async_updates(
current_time_millis(),
[RecordUpdate(record, None) for record in records],
)
# Now for the add
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"ip_addresses": ["127.0.0.1"],
"name": "foo3._fakeservice._tcp.local.",
"port": 1234,
"properties": {},
"type": "_fakeservice._tcp.local.",
}
]
}
# Now for the update
async with asyncio.timeout(1):
response = await client.receive_json()
assert response["event"] == {
"add": [
{
"ip_addresses": ["127.0.0.1"],
"name": "foo3._fakeservice._tcp.local.",
"port": 1234,
"properties": {},
"type": "_fakeservice._tcp.local.",
}
]
}
# now move time forward and remove the record
future = current_time_millis() + (4500 * 1000)
records = instance.zeroconf.cache.async_expire(future)
record_updates = [RecordUpdate(record, record) for record in records]
instance.zeroconf.record_manager.async_updates(future, record_updates)
instance.zeroconf.record_manager.async_updates_complete(True)
removes: set[str] = set()
for _ in range(3):
async with asyncio.timeout(1):
response = await client.receive_json()
assert "remove" in response["event"]
removes.add(next(iter(response["event"]["remove"]))["name"])
assert len(removes) == 3
assert removes == {
"foo2._fakeservice._tcp.local.",
"foo3._fakeservice._tcp.local.",
"wrong._wrongservice._tcp.local.",
}