mirror of
https://github.com/home-assistant/supervisor.git
synced 2026-04-02 16:22:51 +01:00
* Add D-Bus tolerant enum base classes to prevent crashes on unknown values D-Bus services (systemd, NetworkManager, RAUC, UDisks2) can introduce new enum values at any time via OS updates. Standard Python enum construction raises ValueError for unknown values, which would crash the Supervisor. Introduce DBusStrEnum and DBusIntEnum base classes that use Python's _missing_ hook to create pseudo-members for unknown values. These pseudo-members pass isinstance checks (satisfying typeguard), preserve the original value, don't pollute __members__, and report unknown values to Sentry (deduplicated per class+value) for observability. Migrate 17 D-Bus enums in dbus/const.py and udisks2/const.py to the new base classes. Enums only sent TO D-Bus (StopUnitMode, StartUnitMode, etc.) are left unchanged. Remove the manual try/except workaround in NetworkInterface.type now that DBusIntEnum handles it automatically. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Add explicit enum conversions for systemd-resolved D-Bus properties The resolved properties (dns_over_tls, dns_stub_listener, dnssec, llmnr, multicast_dns, resolv_conf_mode) were returning raw string values from D-Bus without converting to their declared enum types. This would fail runtime type checking with typeguard. Now safe to add explicit conversions since these enums use DBusStrEnum, which tolerates unknown values from D-Bus without crashing. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Avoid blocking I/O in D-Bus enum Sentry reporting Move sentry_sdk.capture_message out of the event loop by adding a fire_and_forget_capture_message helper that offloads the call to the executor when a running loop is detected. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * Handle exceptions when reporting message to Sentry * Narrow typing of reported values Use str/int explicitly since that is what the two existing Enum classes can actually report. * Adjust test style * Apply suggestions from code review --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
205 lines
7.3 KiB
Python
205 lines
7.3 KiB
Python
"""Test NetwrokInterface."""
|
|
|
|
from ipaddress import IPv4Address, IPv4Interface, IPv6Address, IPv6Interface
|
|
|
|
from dbus_fast.aio.message_bus import MessageBus
|
|
import pytest
|
|
|
|
from supervisor.dbus.const import DeviceType, InterfaceMethod
|
|
from supervisor.dbus.network import NetworkManager
|
|
from supervisor.dbus.network.interface import NetworkInterface
|
|
|
|
from tests.common import mock_dbus_services
|
|
from tests.const import TEST_INTERFACE_ETH_NAME, TEST_INTERFACE_WLAN_NAME
|
|
from tests.dbus_service_mocks.base import DBusServiceMock
|
|
from tests.dbus_service_mocks.network_device import Device as DeviceService
|
|
|
|
|
|
@pytest.fixture(name="device_eth0_service")
|
|
async def fixture_device_eth0_service(
|
|
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
) -> DeviceService:
|
|
"""Mock Device eth0 service."""
|
|
yield network_manager_services["network_device"][
|
|
"/org/freedesktop/NetworkManager/Devices/1"
|
|
]
|
|
|
|
|
|
@pytest.fixture(name="device_wlan0_service")
|
|
async def fixture_device_wlan0_service(
|
|
network_manager_services: dict[str, DBusServiceMock | dict[str, DBusServiceMock]],
|
|
) -> DeviceService:
|
|
"""Mock Device wlan0 service."""
|
|
yield network_manager_services["network_device"][
|
|
"/org/freedesktop/NetworkManager/Devices/3"
|
|
]
|
|
|
|
|
|
@pytest.fixture(name="device_unmanaged_service")
|
|
async def fixture_device_unmanaged_service(
|
|
dbus_session_bus: MessageBus,
|
|
) -> DeviceService:
|
|
"""Mock Device unmanaged service."""
|
|
yield (
|
|
await mock_dbus_services(
|
|
{"network_device": "/org/freedesktop/NetworkManager/Devices/35"},
|
|
dbus_session_bus,
|
|
)
|
|
)["network_device"]
|
|
|
|
|
|
async def test_network_interface_ethernet(
|
|
device_eth0_service: DeviceService, dbus_session_bus: MessageBus
|
|
):
|
|
"""Test network interface."""
|
|
interface = NetworkInterface("/org/freedesktop/NetworkManager/Devices/1")
|
|
|
|
assert interface.sync_properties is False
|
|
assert interface.interface_name is None
|
|
assert interface.type is None
|
|
|
|
await interface.connect(dbus_session_bus)
|
|
|
|
assert interface.sync_properties is True
|
|
assert interface.interface_name == TEST_INTERFACE_ETH_NAME
|
|
assert interface.type == DeviceType.ETHERNET
|
|
assert interface.managed is True
|
|
assert interface.wireless is None
|
|
assert interface.connection.state == 2
|
|
assert interface.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
|
|
|
|
assert interface.connection.ipv4.address == [IPv4Interface("192.168.2.148/24")]
|
|
assert interface.connection.ipv6.address == [
|
|
IPv6Interface("2a03:169:3df5:0:6be9:2588:b26a:a679/64"),
|
|
IPv6Interface("2a03:169:3df5::2f1/128"),
|
|
]
|
|
|
|
assert interface.connection.ipv4.gateway == IPv4Address("192.168.2.1")
|
|
assert interface.connection.ipv6.gateway == IPv6Address("fe80::da58:d7ff:fe00:9c69")
|
|
|
|
assert interface.connection.ipv4.nameservers == [IPv4Address("192.168.2.2")]
|
|
assert interface.connection.ipv6.nameservers == [
|
|
IPv6Address("2001:1620:2777:1::10"),
|
|
IPv6Address("2001:1620:2777:2::20"),
|
|
]
|
|
|
|
assert interface.settings.ipv4.method == InterfaceMethod.AUTO
|
|
assert interface.settings.ipv6.method == InterfaceMethod.AUTO
|
|
assert interface.settings.connection.id == "Wired connection 1"
|
|
|
|
device_eth0_service.emit_properties_changed({"Managed": False})
|
|
await device_eth0_service.ping()
|
|
assert interface.managed is False
|
|
|
|
device_eth0_service.emit_properties_changed({}, ["Managed"])
|
|
await device_eth0_service.ping()
|
|
await device_eth0_service.ping()
|
|
assert interface.managed is True
|
|
|
|
|
|
async def test_network_interface_wlan(
|
|
device_wlan0_service: DeviceService, dbus_session_bus: MessageBus
|
|
):
|
|
"""Test wlan network interface."""
|
|
interface = NetworkInterface("/org/freedesktop/NetworkManager/Devices/3")
|
|
|
|
assert interface.wireless is None
|
|
|
|
await interface.connect(dbus_session_bus)
|
|
|
|
assert interface.sync_properties is True
|
|
assert interface.interface_name == TEST_INTERFACE_WLAN_NAME
|
|
assert interface.type == DeviceType.WIRELESS
|
|
assert interface.wireless is not None
|
|
assert interface.wireless.bitrate == 0
|
|
|
|
|
|
async def test_old_connection_disconnect(
|
|
network_manager: NetworkManager, device_eth0_service: DeviceService
|
|
):
|
|
"""Test old connection disconnects on connection change."""
|
|
interface = network_manager.get(TEST_INTERFACE_ETH_NAME)
|
|
connection = interface.connection
|
|
assert connection.is_connected is True
|
|
|
|
device_eth0_service.emit_properties_changed({"ActiveConnection": "/"})
|
|
await device_eth0_service.ping()
|
|
|
|
assert interface.connection is None
|
|
assert connection.is_connected is False
|
|
|
|
|
|
async def test_old_wireless_disconnect(
|
|
network_manager: NetworkManager, device_wlan0_service: DeviceService
|
|
):
|
|
"""Test old wireless disconnects on type change."""
|
|
interface = network_manager.get(TEST_INTERFACE_WLAN_NAME)
|
|
wireless = interface.wireless
|
|
assert wireless.is_connected is True
|
|
|
|
device_wlan0_service.emit_properties_changed({"DeviceType": DeviceType.ETHERNET})
|
|
await device_wlan0_service.ping()
|
|
|
|
assert interface.wireless is None
|
|
assert wireless.is_connected is False
|
|
|
|
|
|
async def test_unmanaged_interface(
|
|
device_unmanaged_service: DeviceService, dbus_session_bus: MessageBus
|
|
):
|
|
"""Test unmanaged interfaces don't sync properties."""
|
|
interface = NetworkInterface("/org/freedesktop/NetworkManager/Devices/35")
|
|
await interface.connect(dbus_session_bus)
|
|
|
|
assert interface.managed is False
|
|
assert interface.connection is None
|
|
assert interface.driver == "veth"
|
|
assert interface.sync_properties is False
|
|
|
|
device_unmanaged_service.emit_properties_changed({"Driver": "test"})
|
|
await device_unmanaged_service.ping()
|
|
assert interface.driver == "veth"
|
|
|
|
|
|
async def test_interface_becomes_unmanaged(
|
|
network_manager: NetworkManager,
|
|
device_eth0_service: DeviceService,
|
|
device_wlan0_service: DeviceService,
|
|
):
|
|
"""Test managed objects disconnect when interface becomes unmanaged."""
|
|
eth0 = network_manager.get(TEST_INTERFACE_ETH_NAME)
|
|
connection = eth0.connection
|
|
wlan0 = network_manager.get(TEST_INTERFACE_WLAN_NAME)
|
|
wireless = wlan0.wireless
|
|
|
|
assert connection.is_connected is True
|
|
assert wireless.is_connected is True
|
|
|
|
device_eth0_service.emit_properties_changed({"Managed": False})
|
|
await device_eth0_service.ping()
|
|
device_wlan0_service.emit_properties_changed({"Managed": False})
|
|
await device_wlan0_service.ping()
|
|
|
|
assert wlan0.wireless is None
|
|
assert wireless.is_connected is False
|
|
assert eth0.connection is None
|
|
assert connection.is_connected is False
|
|
|
|
|
|
async def test_unknown_device_type(
|
|
device_eth0_service: DeviceService, dbus_session_bus: MessageBus
|
|
):
|
|
"""Test unknown device types are handled gracefully."""
|
|
interface = NetworkInterface("/org/freedesktop/NetworkManager/Devices/1")
|
|
await interface.connect(dbus_session_bus)
|
|
|
|
# Emit an unknown device type (e.g., 1000 which doesn't exist in the enum)
|
|
device_eth0_service.emit_properties_changed({"DeviceType": 1000})
|
|
await device_eth0_service.ping()
|
|
|
|
# Should preserve the actual value as a pseudo-member instead of crashing
|
|
assert isinstance(interface.type, DeviceType)
|
|
assert interface.type == 1000
|
|
# Wireless should be None since it's not a wireless device
|
|
assert interface.wireless is None
|