Improve connection settings tests (#5278)

* Improve connection settings fixture

Make the connection settings fixture behave more closely to the actual
NetworkManager. The behavior has been tested with NetworkManager 1.42.4
(Debian 12) and 1.44.2 (HAOS 13.1). This likely behaves similar in older
versions too.

* Introduce separate skeleton and settings for wireless

Instead of having a combined network settings object which has
Ethernet and Wirless settings, create a separate settings object for
wireless.

* Handle addresses/address-data property like NetworkManager

* Address ruff check

* Improve network API test

Add a test which changes from "static" to "auto". Validate that settings
are updated accordingly. Specifically, today this does clear the DNS
setting (by not providing the property).

* ruff format

* ruff check

* Complete TEST_INTERFACE rename

* Add partial network update as test case
This commit is contained in:
Stefan Agner 2024-08-30 16:07:04 +02:00 committed by GitHub
parent 2be84e1282
commit c0e35376f3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 376 additions and 133 deletions

View File

@ -1,4 +1,4 @@
"""Test NetwrokInterface API.""" """Test network API."""
from unittest.mock import AsyncMock, patch from unittest.mock import AsyncMock, patch
@ -9,7 +9,11 @@ import pytest
from supervisor.const import DOCKER_NETWORK, DOCKER_NETWORK_MASK from supervisor.const import DOCKER_NETWORK, DOCKER_NETWORK_MASK
from supervisor.coresys import CoreSys from supervisor.coresys import CoreSys
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN from tests.const import (
TEST_INTERFACE_ETH_MAC,
TEST_INTERFACE_ETH_NAME,
TEST_INTERFACE_WLAN_NAME,
)
from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_connection_settings import ( from tests.dbus_service_mocks.network_connection_settings import (
ConnectionSettings as ConnectionSettingsService, ConnectionSettings as ConnectionSettingsService,
@ -24,19 +28,19 @@ async def test_api_network_info(api_client: TestClient, coresys: CoreSys):
"""Test network manager api.""" """Test network manager api."""
resp = await api_client.get("/network/info") resp = await api_client.get("/network/info")
result = await resp.json() result = await resp.json()
assert TEST_INTERFACE in ( assert TEST_INTERFACE_ETH_NAME in (
inet["interface"] for inet in result["data"]["interfaces"] inet["interface"] for inet in result["data"]["interfaces"]
) )
assert TEST_INTERFACE_WLAN in ( assert TEST_INTERFACE_WLAN_NAME in (
inet["interface"] for inet in result["data"]["interfaces"] inet["interface"] for inet in result["data"]["interfaces"]
) )
for interface in result["data"]["interfaces"]: for interface in result["data"]["interfaces"]:
if interface["interface"] == TEST_INTERFACE: if interface["interface"] == TEST_INTERFACE_ETH_NAME:
assert interface["primary"] assert interface["primary"]
assert interface["ipv4"]["gateway"] == "192.168.2.1" assert interface["ipv4"]["gateway"] == "192.168.2.1"
assert interface["mac"] == "AA:BB:CC:DD:EE:FF" assert interface["mac"] == "AA:BB:CC:DD:EE:FF"
if interface["interface"] == TEST_INTERFACE_WLAN: if interface["interface"] == TEST_INTERFACE_WLAN_NAME:
assert not interface["primary"] assert not interface["primary"]
assert interface["mac"] == "FF:EE:DD:CC:BB:AA" assert interface["mac"] == "FF:EE:DD:CC:BB:AA"
assert interface["ipv4"] == { assert interface["ipv4"] == {
@ -60,10 +64,12 @@ async def test_api_network_info(api_client: TestClient, coresys: CoreSys):
assert result["data"]["docker"]["gateway"] == str(coresys.docker.network.gateway) assert result["data"]["docker"]["gateway"] == str(coresys.docker.network.gateway)
@pytest.mark.parametrize("intr_id", [TEST_INTERFACE, "AA:BB:CC:DD:EE:FF"]) @pytest.mark.parametrize(
async def test_api_network_interface_info(api_client: TestClient, intr_id: str): "interface_id", [TEST_INTERFACE_ETH_NAME, TEST_INTERFACE_ETH_MAC]
)
async def test_api_network_interface_info(api_client: TestClient, interface_id: str):
"""Test network manager api.""" """Test network manager api."""
resp = await api_client.get(f"/network/interface/{intr_id}/info") resp = await api_client.get(f"/network/interface/{interface_id}/info")
result = await resp.json() result = await resp.json()
assert result["data"]["ipv4"]["address"][-1] == "192.168.2.148/24" assert result["data"]["ipv4"]["address"][-1] == "192.168.2.148/24"
assert result["data"]["ipv4"]["gateway"] == "192.168.2.1" assert result["data"]["ipv4"]["gateway"] == "192.168.2.1"
@ -79,7 +85,7 @@ async def test_api_network_interface_info(api_client: TestClient, intr_id: str):
"2001:1620:2777:2::20", "2001:1620:2777:2::20",
] ]
assert result["data"]["ipv6"]["ready"] is True assert result["data"]["ipv6"]["ready"] is True
assert result["data"]["interface"] == TEST_INTERFACE assert result["data"]["interface"] == TEST_INTERFACE_ETH_NAME
async def test_api_network_interface_info_default(api_client: TestClient): async def test_api_network_interface_info_default(api_client: TestClient):
@ -100,24 +106,26 @@ async def test_api_network_interface_info_default(api_client: TestClient):
"2001:1620:2777:2::20", "2001:1620:2777:2::20",
] ]
assert result["data"]["ipv6"]["ready"] is True assert result["data"]["ipv6"]["ready"] is True
assert result["data"]["interface"] == TEST_INTERFACE assert result["data"]["interface"] == TEST_INTERFACE_ETH_NAME
@pytest.mark.parametrize("intr_id", [TEST_INTERFACE, "AA:BB:CC:DD:EE:FF"]) @pytest.mark.parametrize(
async def test_api_network_interface_update( "interface_id", [TEST_INTERFACE_ETH_NAME, TEST_INTERFACE_ETH_MAC]
)
async def test_api_network_interface_update_mac_or_name(
api_client: TestClient, api_client: TestClient,
coresys: CoreSys, coresys: CoreSys,
network_manager_service: NetworkManagerService, network_manager_service: NetworkManagerService,
connection_settings_service: ConnectionSettingsService, connection_settings_service: ConnectionSettingsService,
intr_id: str, interface_id: str,
): ):
"""Test network manager api.""" """Test network manager API update with name or MAC address."""
network_manager_service.CheckConnectivity.calls.clear() network_manager_service.CheckConnectivity.calls.clear()
connection_settings_service.Update.calls.clear() connection_settings_service.Update.calls.clear()
assert coresys.dbus.network.get(TEST_INTERFACE).settings.ipv4.method == "auto" assert coresys.dbus.network.get(interface_id).settings.ipv4.method == "auto"
resp = await api_client.post( resp = await api_client.post(
f"/network/interface/{intr_id}/update", f"/network/interface/{interface_id}/update",
json={ json={
"ipv4": { "ipv4": {
"method": "static", "method": "static",
@ -133,14 +141,101 @@ async def test_api_network_interface_update(
assert len(connection_settings_service.Update.calls) == 1 assert len(connection_settings_service.Update.calls) == 1
await connection_settings_service.ping() await connection_settings_service.ping()
await connection_settings_service.ping() assert (
assert coresys.dbus.network.get(TEST_INTERFACE).settings.ipv4.method == "manual" coresys.dbus.network.get(TEST_INTERFACE_ETH_NAME).settings.ipv4.method
== "manual"
)
async def test_api_network_interface_update_ethernet(
api_client: TestClient,
coresys: CoreSys,
network_manager_service: NetworkManagerService,
connection_settings_service: ConnectionSettingsService,
):
"""Test network manager API update with name or MAC address."""
network_manager_service.CheckConnectivity.calls.clear()
connection_settings_service.Update.calls.clear()
# Full static configuration (represents frontend static config)
resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/update",
json={
"ipv4": {
"method": "static",
"nameservers": ["1.1.1.1"],
"address": ["192.168.2.148/24"],
"gateway": "192.168.2.1",
}
},
)
result = await resp.json()
assert result["result"] == "ok"
assert network_manager_service.CheckConnectivity.calls == [()]
assert len(connection_settings_service.Update.calls) == 1
settings = connection_settings_service.Update.calls[0][0]
assert "ipv4" in settings
assert settings["ipv4"]["method"] == Variant("s", "manual")
assert settings["ipv4"]["address-data"] == Variant(
"aa{sv}",
[{"address": Variant("s", "192.168.2.148"), "prefix": Variant("u", 24)}],
)
assert settings["ipv4"]["dns"] == Variant("au", [16843009])
assert settings["ipv4"]["gateway"] == Variant("s", "192.168.2.1")
# Partial static configuration, updates only provided settings (e.g. by CLI)
resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/update",
json={
"ipv4": {
"method": "static",
"address": ["192.168.2.149/24"],
}
},
)
result = await resp.json()
assert result["result"] == "ok"
assert len(connection_settings_service.Update.calls) == 2
settings = connection_settings_service.Update.calls[1][0]
assert "ipv4" in settings
assert settings["ipv4"]["method"] == Variant("s", "manual")
assert settings["ipv4"]["address-data"] == Variant(
"aa{sv}",
[{"address": Variant("s", "192.168.2.149"), "prefix": Variant("u", 24)}],
)
assert settings["ipv4"]["dns"] == Variant("au", [16843009])
assert settings["ipv4"]["gateway"] == Variant("s", "192.168.2.1")
# Auto configuration, clears all settings (represents frontend auto config)
resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/update",
json={
"ipv4": {
"method": "auto",
}
},
)
result = await resp.json()
assert result["result"] == "ok"
assert len(connection_settings_service.Update.calls) == 3
settings = connection_settings_service.Update.calls[2][0]
# Validate network update to auto clears address, DNS and gateway settings
assert "ipv4" in settings
assert settings["ipv4"]["method"] == Variant("s", "auto")
assert "address-data" not in settings["ipv4"]
assert "addresses" not in settings["ipv4"]
assert "dns-data" not in settings["ipv4"]
assert "dns" not in settings["ipv4"]
assert "gateway" not in settings["ipv4"]
async def test_api_network_interface_update_wifi(api_client: TestClient): async def test_api_network_interface_update_wifi(api_client: TestClient):
"""Test network manager api.""" """Test network manager api."""
resp = await api_client.post( resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE_WLAN}/update", f"/network/interface/{TEST_INTERFACE_WLAN_NAME}/update",
json={ json={
"enabled": True, "enabled": True,
"ipv4": { "ipv4": {
@ -159,7 +254,7 @@ async def test_api_network_interface_update_wifi(api_client: TestClient):
async def test_api_network_interface_update_remove(api_client: TestClient): async def test_api_network_interface_update_remove(api_client: TestClient):
"""Test network manager api.""" """Test network manager api."""
resp = await api_client.post( resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE}/update", f"/network/interface/{TEST_INTERFACE_ETH_NAME}/update",
json={"enabled": False}, json={"enabled": False},
) )
result = await resp.json() result = await resp.json()
@ -181,12 +276,14 @@ async def test_api_network_interface_update_invalid(api_client: TestClient):
result = await resp.json() result = await resp.json()
assert result["message"] == "Interface invalid does not exist" assert result["message"] == "Interface invalid does not exist"
resp = await api_client.post(f"/network/interface/{TEST_INTERFACE}/update", json={}) resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE_ETH_NAME}/update", json={}
)
result = await resp.json() result = await resp.json()
assert result["message"] == "You need to supply at least one option to update" assert result["message"] == "You need to supply at least one option to update"
resp = await api_client.post( resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE}/update", f"/network/interface/{TEST_INTERFACE_ETH_NAME}/update",
json={"ipv4": {"nameservers": "1.1.1.1"}}, json={"ipv4": {"nameservers": "1.1.1.1"}},
) )
result = await resp.json() result = await resp.json()
@ -200,7 +297,7 @@ async def test_api_network_wireless_scan(api_client: TestClient):
"""Test network manager api.""" """Test network manager api."""
with patch("asyncio.sleep", return_value=AsyncMock()): with patch("asyncio.sleep", return_value=AsyncMock()):
resp = await api_client.get( resp = await api_client.get(
f"/network/interface/{TEST_INTERFACE_WLAN}/accesspoints" f"/network/interface/{TEST_INTERFACE_WLAN_NAME}/accesspoints"
) )
result = await resp.json() result = await resp.json()
@ -235,7 +332,8 @@ async def test_api_network_vlan(
settings_service: SettingsService = network_manager_services["network_settings"] settings_service: SettingsService = network_manager_services["network_settings"]
settings_service.AddConnection.calls.clear() settings_service.AddConnection.calls.clear()
resp = await api_client.post( resp = await api_client.post(
f"/network/interface/{TEST_INTERFACE}/vlan/1", json={"ipv4": {"method": "auto"}} f"/network/interface/{TEST_INTERFACE_ETH_NAME}/vlan/1",
json={"ipv4": {"method": "auto"}},
) )
result = await resp.json() result = await resp.json()
assert result["result"] == "ok" assert result["result"] == "ok"

View File

@ -1,7 +1,8 @@
"""Consts for tests.""" """Consts for tests."""
TEST_INTERFACE = "eth0" TEST_INTERFACE_ETH_NAME = "eth0"
TEST_INTERFACE_WLAN = "wlan0" TEST_INTERFACE_ETH_MAC = "AA:BB:CC:DD:EE:FF"
TEST_INTERFACE_WLAN_NAME = "wlan0"
TEST_WS_URL = "ws://test.org:3000" TEST_WS_URL = "ws://test.org:3000"
TEST_ADDON_SLUG = "local_ssh" TEST_ADDON_SLUG = "local_ssh"

View File

@ -9,12 +9,12 @@ from supervisor.host.configuration import IpConfig, IpSetting, VlanConfig
from supervisor.host.const import InterfaceMethod, InterfaceType from supervisor.host.const import InterfaceMethod, InterfaceType
from supervisor.host.network import Interface from supervisor.host.network import Interface
from tests.const import TEST_INTERFACE from tests.const import TEST_INTERFACE_ETH_NAME
async def test_get_connection_from_interface(network_manager: NetworkManager): async def test_get_connection_from_interface(network_manager: NetworkManager):
"""Test network interface.""" """Test network interface."""
dbus_interface = network_manager.get(TEST_INTERFACE) dbus_interface = network_manager.get(TEST_INTERFACE_ETH_NAME)
interface = Interface.from_dbus_interface(dbus_interface) interface = Interface.from_dbus_interface(dbus_interface)
connection_payload = get_connection_from_interface(interface, network_manager) connection_payload = get_connection_from_interface(interface, network_manager)
@ -33,7 +33,7 @@ async def test_get_connection_from_interface(network_manager: NetworkManager):
async def test_get_connection_no_path(network_manager: NetworkManager): async def test_get_connection_no_path(network_manager: NetworkManager):
"""Test network interface without a path.""" """Test network interface without a path."""
dbus_interface = network_manager.get(TEST_INTERFACE) dbus_interface = network_manager.get(TEST_INTERFACE_ETH_NAME)
with patch.object(NetworkInterface, "path", new=PropertyMock(return_value=None)): with patch.object(NetworkInterface, "path", new=PropertyMock(return_value=None)):
interface = Interface.from_dbus_interface(dbus_interface) interface = Interface.from_dbus_interface(dbus_interface)

View File

@ -16,6 +16,10 @@ from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_connection_settings import ( from tests.dbus_service_mocks.network_connection_settings import (
ConnectionSettings as ConnectionSettingsService, ConnectionSettings as ConnectionSettingsService,
) )
from tests.dbus_service_mocks.network_device import (
ETHERNET_DEVICE_OBJECT_PATH,
WIRELESS_DEVICE_OBJECT_PATH,
)
@pytest.fixture(name="connection_settings_service", autouse=True) @pytest.fixture(name="connection_settings_service", autouse=True)
@ -27,14 +31,21 @@ async def fixture_connection_settings_service(
@pytest.fixture(name="dbus_interface") @pytest.fixture(name="dbus_interface")
async def fixture_dbus_interface(dbus_session_bus: MessageBus) -> NetworkInterface: async def fixture_dbus_interface(
dbus_session_bus: MessageBus, device_object_path: str = ETHERNET_DEVICE_OBJECT_PATH
) -> NetworkInterface:
"""Get connected dbus interface.""" """Get connected dbus interface."""
dbus_interface = NetworkInterface("/org/freedesktop/NetworkManager/Devices/1") dbus_interface = NetworkInterface(device_object_path)
await dbus_interface.connect(dbus_session_bus) await dbus_interface.connect(dbus_session_bus)
yield dbus_interface yield dbus_interface
async def test_update( @pytest.mark.parametrize(
"dbus_interface",
[ETHERNET_DEVICE_OBJECT_PATH, WIRELESS_DEVICE_OBJECT_PATH],
indirect=True,
)
async def test_ethernet_update(
dbus_interface: NetworkInterface, dbus_interface: NetworkInterface,
connection_settings_service: ConnectionSettingsService, connection_settings_service: ConnectionSettingsService,
): ):
@ -91,16 +102,22 @@ async def test_update(
assert "proxy" in settings assert "proxy" in settings
assert "vlan" not in settings
if settings["connection"]["type"] == "802-3-ethernet":
assert "802-3-ethernet" in settings assert "802-3-ethernet" in settings
assert settings["802-3-ethernet"]["auto-negotiate"] == Variant("b", False) assert settings["802-3-ethernet"]["auto-negotiate"] == Variant("b", False)
assert "802-11-wireless" not in settings
assert "802-11-wireless-security" not in settings
if settings["connection"]["type"] == "802-11-wireless":
assert "802-11-wireless" in settings assert "802-11-wireless" in settings
assert settings["802-11-wireless"]["ssid"] == Variant("ay", b"NETT") assert settings["802-11-wireless"]["ssid"] == Variant("ay", b"NETT")
assert "mode" not in settings["802-11-wireless"] assert "mode" not in settings["802-11-wireless"]
assert "powersave" not in settings["802-11-wireless"] assert "powersave" not in settings["802-11-wireless"]
assert "802-11-wireless-security" not in settings assert "802-11-wireless-security" not in settings
assert "vlan" not in settings
async def test_ipv6_disabled_is_link_local(dbus_interface: NetworkInterface): async def test_ipv6_disabled_is_link_local(dbus_interface: NetworkInterface):

View File

@ -7,7 +7,7 @@ from supervisor.dbus.const import ConnectionStateFlags
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.connection import NetworkConnection from supervisor.dbus.network.connection import NetworkConnection
from tests.const import TEST_INTERFACE from tests.const import TEST_INTERFACE_ETH_NAME
from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_active_connection import ( from tests.dbus_service_mocks.network_active_connection import (
ActiveConnection as ActiveConnectionService, ActiveConnection as ActiveConnectionService,
@ -57,7 +57,7 @@ async def test_old_ipv4_disconnect(
network_manager: NetworkManager, active_connection_service: ActiveConnectionService network_manager: NetworkManager, active_connection_service: ActiveConnectionService
): ):
"""Test old ipv4 disconnects on ipv4 change.""" """Test old ipv4 disconnects on ipv4 change."""
connection = network_manager.get(TEST_INTERFACE).connection connection = network_manager.get(TEST_INTERFACE_ETH_NAME).connection
ipv4 = connection.ipv4 ipv4 = connection.ipv4
assert ipv4.is_connected is True assert ipv4.is_connected is True
@ -72,7 +72,7 @@ async def test_old_ipv6_disconnect(
network_manager: NetworkManager, active_connection_service: ActiveConnectionService network_manager: NetworkManager, active_connection_service: ActiveConnectionService
): ):
"""Test old ipv6 disconnects on ipv6 change.""" """Test old ipv6 disconnects on ipv6 change."""
connection = network_manager.get(TEST_INTERFACE).connection connection = network_manager.get(TEST_INTERFACE_ETH_NAME).connection
ipv6 = connection.ipv6 ipv6 = connection.ipv6
assert ipv6.is_connected is True assert ipv6.is_connected is True
@ -87,7 +87,7 @@ async def test_old_settings_disconnect(
network_manager: NetworkManager, active_connection_service: ActiveConnectionService network_manager: NetworkManager, active_connection_service: ActiveConnectionService
): ):
"""Test old settings disconnects on settings change.""" """Test old settings disconnects on settings change."""
connection = network_manager.get(TEST_INTERFACE).connection connection = network_manager.get(TEST_INTERFACE_ETH_NAME).connection
settings = connection.settings settings = connection.settings
assert settings.is_connected is True assert settings.is_connected is True

View File

@ -10,7 +10,7 @@ from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.interface import NetworkInterface from supervisor.dbus.network.interface import NetworkInterface
from tests.common import mock_dbus_services from tests.common import mock_dbus_services
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN 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.base import DBusServiceMock
from tests.dbus_service_mocks.network_device import Device as DeviceService from tests.dbus_service_mocks.network_device import Device as DeviceService
@ -61,7 +61,7 @@ async def test_network_interface_ethernet(
await interface.connect(dbus_session_bus) await interface.connect(dbus_session_bus)
assert interface.sync_properties is True assert interface.sync_properties is True
assert interface.name == TEST_INTERFACE assert interface.name == TEST_INTERFACE_ETH_NAME
assert interface.type == DeviceType.ETHERNET assert interface.type == DeviceType.ETHERNET
assert interface.managed is True assert interface.managed is True
assert interface.wireless is None assert interface.wireless is None
@ -108,7 +108,7 @@ async def test_network_interface_wlan(
await interface.connect(dbus_session_bus) await interface.connect(dbus_session_bus)
assert interface.sync_properties is True assert interface.sync_properties is True
assert interface.name == TEST_INTERFACE_WLAN assert interface.name == TEST_INTERFACE_WLAN_NAME
assert interface.type == DeviceType.WIRELESS assert interface.type == DeviceType.WIRELESS
assert interface.wireless is not None assert interface.wireless is not None
assert interface.wireless.bitrate == 0 assert interface.wireless.bitrate == 0
@ -118,7 +118,7 @@ async def test_old_connection_disconnect(
network_manager: NetworkManager, device_eth0_service: DeviceService network_manager: NetworkManager, device_eth0_service: DeviceService
): ):
"""Test old connection disconnects on connection change.""" """Test old connection disconnects on connection change."""
interface = network_manager.get(TEST_INTERFACE) interface = network_manager.get(TEST_INTERFACE_ETH_NAME)
connection = interface.connection connection = interface.connection
assert connection.is_connected is True assert connection.is_connected is True
@ -133,7 +133,7 @@ async def test_old_wireless_disconnect(
network_manager: NetworkManager, device_wlan0_service: DeviceService network_manager: NetworkManager, device_wlan0_service: DeviceService
): ):
"""Test old wireless disconnects on type change.""" """Test old wireless disconnects on type change."""
interface = network_manager.get(TEST_INTERFACE_WLAN) interface = network_manager.get(TEST_INTERFACE_WLAN_NAME)
wireless = interface.wireless wireless = interface.wireless
assert wireless.is_connected is True assert wireless.is_connected is True
@ -167,9 +167,9 @@ async def test_interface_becomes_unmanaged(
device_wlan0_service: DeviceService, device_wlan0_service: DeviceService,
): ):
"""Test managed objects disconnect when interface becomes unmanaged.""" """Test managed objects disconnect when interface becomes unmanaged."""
eth0 = network_manager.get(TEST_INTERFACE) eth0 = network_manager.get(TEST_INTERFACE_ETH_NAME)
connection = eth0.connection connection = eth0.connection
wlan0 = network_manager.get(TEST_INTERFACE_WLAN) wlan0 = network_manager.get(TEST_INTERFACE_WLAN_NAME)
wireless = wlan0.wireless wireless = wlan0.wireless
assert connection.is_connected is True assert connection.is_connected is True

View File

@ -17,9 +17,9 @@ from supervisor.exceptions import (
) )
from supervisor.utils.dbus import DBus from supervisor.utils.dbus import DBus
from tests.const import TEST_INTERFACE, TEST_INTERFACE_WLAN 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.base import DBusServiceMock
from tests.dbus_service_mocks.network_connection_settings import SETTINGS_FIXTURE from tests.dbus_service_mocks.network_connection_settings import SETTINGS_1_FIXTURE
from tests.dbus_service_mocks.network_manager import ( from tests.dbus_service_mocks.network_manager import (
NetworkManager as NetworkManagerService, NetworkManager as NetworkManagerService,
) )
@ -43,7 +43,7 @@ async def test_network_manager(
await network_manager.connect(dbus_session_bus) await network_manager.connect(dbus_session_bus)
assert TEST_INTERFACE in network_manager assert TEST_INTERFACE_ETH_NAME in network_manager
assert network_manager.connectivity_enabled is True assert network_manager.connectivity_enabled is True
network_manager_service.emit_properties_changed({"ConnectivityCheckEnabled": False}) network_manager_service.emit_properties_changed({"ConnectivityCheckEnabled": False})
@ -113,7 +113,7 @@ async def test_add_and_activate_connection(
network_manager_service.AddAndActivateConnection.calls.clear() network_manager_service.AddAndActivateConnection.calls.clear()
settings, connection = await network_manager.add_and_activate_connection( settings, connection = await network_manager.add_and_activate_connection(
SETTINGS_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1" SETTINGS_1_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1"
) )
assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6" assert settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert settings.ipv4.method == "auto" assert settings.ipv4.method == "auto"
@ -122,7 +122,7 @@ async def test_add_and_activate_connection(
connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1" connection.settings.object_path == "/org/freedesktop/NetworkManager/Settings/1"
) )
assert network_manager_service.AddAndActivateConnection.calls == [ assert network_manager_service.AddAndActivateConnection.calls == [
(SETTINGS_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1", "/") (SETTINGS_1_FIXTURE, "/org/freedesktop/NetworkManager/Devices/1", "/")
] ]
@ -130,13 +130,13 @@ async def test_removed_devices_disconnect(
network_manager_service: NetworkManagerService, network_manager: NetworkManager network_manager_service: NetworkManagerService, network_manager: NetworkManager
): ):
"""Test removed devices are disconnected.""" """Test removed devices are disconnected."""
wlan = network_manager.get(TEST_INTERFACE_WLAN) wlan = network_manager.get(TEST_INTERFACE_WLAN_NAME)
assert wlan.is_connected is True assert wlan.is_connected is True
network_manager_service.emit_properties_changed({"Devices": []}) network_manager_service.emit_properties_changed({"Devices": []})
await network_manager_service.ping() await network_manager_service.ping()
assert TEST_INTERFACE_WLAN not in network_manager assert TEST_INTERFACE_WLAN_NAME not in network_manager
assert wlan.is_connected is False assert wlan.is_connected is False

View File

@ -7,7 +7,7 @@ from supervisor.dbus.network.settings import NetworkManagerSettings
from supervisor.exceptions import DBusNotConnectedError from supervisor.exceptions import DBusNotConnectedError
from tests.common import mock_dbus_services from tests.common import mock_dbus_services
from tests.dbus_service_mocks.network_connection_settings import SETTINGS_FIXTURE from tests.dbus_service_mocks.network_connection_settings import SETTINGS_1_FIXTURE
from tests.dbus_service_mocks.network_settings import Settings as SettingsService from tests.dbus_service_mocks.network_settings import Settings as SettingsService
@ -30,15 +30,15 @@ async def test_add_connection(
settings = NetworkManagerSettings() settings = NetworkManagerSettings()
with pytest.raises(DBusNotConnectedError): with pytest.raises(DBusNotConnectedError):
await settings.add_connection(SETTINGS_FIXTURE) await settings.add_connection(SETTINGS_1_FIXTURE)
await settings.connect(dbus_session_bus) await settings.connect(dbus_session_bus)
connection_settings = await settings.add_connection(SETTINGS_FIXTURE) connection_settings = await settings.add_connection(SETTINGS_1_FIXTURE)
assert connection_settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6" assert connection_settings.connection.uuid == "0c23631e-2118-355c-bbb0-8943229cb0d6"
assert connection_settings.ipv4.method == "auto" assert connection_settings.ipv4.method == "auto"
assert settings_service.AddConnection.calls == [(SETTINGS_FIXTURE,)] assert settings_service.AddConnection.calls == [(SETTINGS_1_FIXTURE,)]
async def test_reload_connections( async def test_reload_connections(

View File

@ -6,7 +6,7 @@ import pytest
from supervisor.dbus.network import NetworkManager from supervisor.dbus.network import NetworkManager
from supervisor.dbus.network.wireless import NetworkWireless from supervisor.dbus.network.wireless import NetworkWireless
from tests.const import TEST_INTERFACE_WLAN from tests.const import TEST_INTERFACE_WLAN_NAME
from tests.dbus_service_mocks.base import DBusServiceMock from tests.dbus_service_mocks.base import DBusServiceMock
from tests.dbus_service_mocks.network_device_wireless import ( from tests.dbus_service_mocks.network_device_wireless import (
DeviceWireless as DeviceWirelessService, DeviceWireless as DeviceWirelessService,
@ -56,7 +56,8 @@ async def test_request_scan(
"""Test request scan.""" """Test request scan."""
device_wireless_service.RequestScan.calls.clear() device_wireless_service.RequestScan.calls.clear()
assert ( assert (
await network_manager.get(TEST_INTERFACE_WLAN).wireless.request_scan() is None await network_manager.get(TEST_INTERFACE_WLAN_NAME).wireless.request_scan()
is None
) )
assert device_wireless_service.RequestScan.calls == [({},)] assert device_wireless_service.RequestScan.calls == [({},)]
@ -64,7 +65,7 @@ async def test_request_scan(
async def test_get_all_access_points(network_manager: NetworkManager): async def test_get_all_access_points(network_manager: NetworkManager):
"""Test get all access points.""" """Test get all access points."""
accesspoints = await network_manager.get( accesspoints = await network_manager.get(
TEST_INTERFACE_WLAN TEST_INTERFACE_WLAN_NAME
).wireless.get_all_accesspoints() ).wireless.get_all_accesspoints()
assert len(accesspoints) == 2 assert len(accesspoints) == 2
assert accesspoints[0].mac == "E4:57:40:A9:D7:DE" assert accesspoints[0].mac == "E4:57:40:A9:D7:DE"
@ -75,7 +76,7 @@ async def test_get_all_access_points(network_manager: NetworkManager):
async def test_old_active_ap_disconnects(network_manager: NetworkManager): async def test_old_active_ap_disconnects(network_manager: NetworkManager):
"""Test old access point disconnects on active ap change.""" """Test old access point disconnects on active ap change."""
wireless = network_manager.get(TEST_INTERFACE_WLAN).wireless wireless = network_manager.get(TEST_INTERFACE_WLAN_NAME).wireless
await wireless.update( await wireless.update(
{"ActiveAccessPoint": "/org/freedesktop/NetworkManager/AccessPoint/43099"} {"ActiveAccessPoint": "/org/freedesktop/NetworkManager/AccessPoint/43099"}

View File

@ -48,6 +48,12 @@ FIXTURES: dict[str, ActiveConnectionFixture] = {
"/org/freedesktop/NetworkManager/Devices/5", "/org/freedesktop/NetworkManager/Devices/5",
], ],
), ),
"/org/freedesktop/NetworkManager/ActiveConnection/3": ActiveConnectionFixture(
connection="/org/freedesktop/NetworkManager/Settings/3",
devices=[
"/org/freedesktop/NetworkManager/Devices/3",
],
),
} }

View File

@ -1,6 +1,10 @@
"""Mock of Network Manager Connection Settings service.""" """Mock of Network Manager Connection Settings service."""
from dbus_fast import Variant from copy import deepcopy
from ipaddress import IPv4Address, IPv6Address
import socket
from dbus_fast import DBusError, Variant
from dbus_fast.service import PropertyAccess, dbus_property, signal from dbus_fast.service import PropertyAccess, dbus_property, signal
from .base import DBusServiceMock, dbus_method from .base import DBusServiceMock, dbus_method
@ -8,13 +12,76 @@ from .base import DBusServiceMock, dbus_method
BUS_NAME = "org.freedesktop.NetworkManager" BUS_NAME = "org.freedesktop.NetworkManager"
DEFAULT_OBJECT_PATH = "/org/freedesktop/NetworkManager/Settings/1" DEFAULT_OBJECT_PATH = "/org/freedesktop/NetworkManager/Settings/1"
SETTINGS_FIXTURE: dict[str, dict[str, Variant]] = { # NetworkManager Connection settings skeleton which gets generated automatically
# Created with 1.42.4, using:
# nmcli con add type ethernet con-name "Test"
# busctl call org.freedesktop.NetworkManager /org/freedesktop/NetworkManager/Settings/5 org.freedesktop.NetworkManager.Settings.Connection GetSettings --json=pretty
# Note that "id" and "type" seem to be the bare minimum an update call, so they can be
# ommitted here.
MINIMAL_SETTINGS_FIXTURE = {
"ipv4": {
"address-data": Variant("aa{sv}", []),
"addresses": Variant("aau", []),
"dns-search": Variant("as", []),
"method": Variant("s", "auto"),
"route-data": Variant("aa{sv}", []),
"routes": Variant("aau", []),
},
"ipv6": {
"address-data": Variant("aa{sv}", []),
"addresses": Variant("a(ayuay)", []),
"dns-search": Variant("as", []),
"method": Variant("s", "auto"),
"route-data": Variant("aa{sv}", []),
"routes": Variant("a(ayuayu)", []),
},
"proxy": {},
}
MINIMAL_ETHERNET_SETTINGS_FIXTURE = MINIMAL_SETTINGS_FIXTURE | {
"connection": {
"permissions": Variant("as", []),
"uuid": Variant("s", "ee736ea0-e2cc-4cc5-9c35-d6df94a56b47"),
},
"802-3-ethernet": {
"auto-negotiate": Variant("b", False),
"mac-address-blacklist": Variant("as", []),
"s390-options": Variant("a{ss}", {}),
},
}
MINIMAL_WIRELESS_SETTINGS_FIXTURE = MINIMAL_SETTINGS_FIXTURE | {
"connection": {
"permissions": Variant("as", []),
"uuid": Variant("s", "bf9f098a-23f5-41b0-873b-b449c58df499"),
},
"802-11-wireless": {
"mac-address-blacklist": Variant("as", []),
"seen-bssids": Variant("as", []),
"ssid": Variant("ay", b"TestSSID"),
},
}
def settings_update(minimal_setting, new_settings):
"""Update Connection settings with minimal skeleton in mind."""
settings = deepcopy(minimal_setting)
for k, v in new_settings.items():
if k in settings:
settings[k].update(v)
else:
settings[k] = v
return settings
SETTINGS_1_FIXTURE: dict[str, dict[str, Variant]] = settings_update(
MINIMAL_ETHERNET_SETTINGS_FIXTURE,
{
"connection": { "connection": {
"id": Variant("s", "Wired connection 1"), "id": Variant("s", "Wired connection 1"),
"interface-name": Variant("s", "eth0"), "interface-name": Variant("s", "eth0"),
"llmnr": Variant("i", 2), "llmnr": Variant("i", 2),
"mdns": Variant("i", 2), "mdns": Variant("i", 2),
"permissions": Variant("as", []),
"timestamp": Variant("t", 1598125548), "timestamp": Variant("t", 1598125548),
"type": Variant("s", "802-3-ethernet"), "type": Variant("s", "802-3-ethernet"),
"uuid": Variant("s", "0c23631e-2118-355c-bbb0-8943229cb0d6"), "uuid": Variant("s", "0c23631e-2118-355c-bbb0-8943229cb0d6"),
@ -29,10 +96,9 @@ SETTINGS_FIXTURE: dict[str, dict[str, Variant]] = {
} }
], ],
), ),
"addresses": Variant("aau", [[2483202240, 24, 16951488]]), "addresses": Variant("aau", [[2483202240, 24, 0]]),
"dns": Variant("au", [16951488]), "dns": Variant("au", [16951488]),
"dns-data": Variant("as", ["192.168.2.1"]), "dns-data": Variant("as", ["192.168.2.1"]),
"dns-search": Variant("as", []),
"gateway": Variant("s", "192.168.2.1"), "gateway": Variant("s", "192.168.2.1"),
"method": Variant("s", "auto"), "method": Variant("s", "auto"),
"route-data": Variant( "route-data": Variant(
@ -48,38 +114,38 @@ SETTINGS_FIXTURE: dict[str, dict[str, Variant]] = {
"routes": Variant("aau", [[8038592, 24, 17435146, 0]]), "routes": Variant("aau", [[8038592, 24, 17435146, 0]]),
}, },
"ipv6": { "ipv6": {
"address-data": Variant("aa{sv}", []),
"addresses": Variant("a(ayuay)", []),
"dns-search": Variant("as", []),
"method": Variant("s", "auto"), "method": Variant("s", "auto"),
"route-data": Variant("aa{sv}", []), "dns": Variant("aay", [IPv6Address("2001:4860:4860::8888").packed]),
"routes": Variant("a(ayuayu)", []), "dns-data": Variant("as", ["2001:4860:4860::8888"]),
"addr-gen-mode": Variant("i", 0), "addr-gen-mode": Variant("i", 0),
}, },
"proxy": {},
"802-3-ethernet": { "802-3-ethernet": {
"assigned-mac-address": Variant("s", "preserve"), "assigned-mac-address": Variant("s", "preserve"),
"auto-negotiate": Variant("b", False),
"mac-address-blacklist": Variant("as", []),
"s390-options": Variant("a{ss}", {}),
}, },
"802-11-wireless": {"ssid": Variant("ay", b"NETT")}, },
} )
SETINGS_FIXTURES: dict[str, dict[str, dict[str, Variant]]] = {
"/org/freedesktop/NetworkManager/Settings/1": SETTINGS_FIXTURE, SETTINGS_2_FIXTURE = settings_update(
"/org/freedesktop/NetworkManager/Settings/2": { MINIMAL_ETHERNET_SETTINGS_FIXTURE,
{
"connection": { "connection": {
k: v k: v
for k, v in SETTINGS_FIXTURE["connection"].items() for k, v in SETTINGS_1_FIXTURE["connection"].items()
if k != "interface-name" if k != "interface-name"
}, },
"ipv4": SETTINGS_FIXTURE["ipv4"], "ipv4": SETTINGS_1_FIXTURE["ipv4"],
"ipv6": SETTINGS_FIXTURE["ipv6"], "ipv6": SETTINGS_1_FIXTURE["ipv6"],
"proxy": {}, "802-3-ethernet": SETTINGS_1_FIXTURE["802-3-ethernet"],
"802-3-ethernet": SETTINGS_FIXTURE["802-3-ethernet"],
"802-11-wireless": SETTINGS_FIXTURE["802-11-wireless"],
"match": {"path": Variant("as", ["platform-ff3f0000.ethernet"])}, "match": {"path": Variant("as", ["platform-ff3f0000.ethernet"])},
}, },
)
SETTINGS_3_FIXTURE = deepcopy(MINIMAL_WIRELESS_SETTINGS_FIXTURE)
SETINGS_FIXTURES: dict[str, dict[str, dict[str, Variant]]] = {
"/org/freedesktop/NetworkManager/Settings/1": SETTINGS_1_FIXTURE,
"/org/freedesktop/NetworkManager/Settings/2": SETTINGS_2_FIXTURE,
"/org/freedesktop/NetworkManager/Settings/3": SETTINGS_3_FIXTURE,
} }
@ -100,7 +166,7 @@ class ConnectionSettings(DBusServiceMock):
"""Initialize object.""" """Initialize object."""
super().__init__() super().__init__()
self.object_path = object_path self.object_path = object_path
self.settings = SETINGS_FIXTURES[object_path] self.settings = deepcopy(SETINGS_FIXTURES[object_path])
@dbus_property(access=PropertyAccess.READ) @dbus_property(access=PropertyAccess.READ)
def Unsaved(self) -> "b": def Unsaved(self) -> "b":
@ -128,7 +194,64 @@ class ConnectionSettings(DBusServiceMock):
@dbus_method() @dbus_method()
def Update(self, properties: "a{sa{sv}}") -> None: def Update(self, properties: "a{sa{sv}}") -> None:
"""Do Update method.""" """Do Update method."""
self.settings = properties if "connection" not in properties:
raise DBusError(
"org.freedesktop.NetworkManager.Settings.Connection.MissingProperty",
"connection.type: property is missing",
)
for required_prop in ("type", "id"):
if required_prop not in properties["connection"]:
raise DBusError(
"org.freedesktop.NetworkManager.Settings.Connection.MissingProperty",
f"connection.{required_prop}: property is missing",
)
if properties["connection"]["type"] == "802-11-wireless":
self.settings = settings_update(
MINIMAL_WIRELESS_SETTINGS_FIXTURE, properties
)
elif properties["connection"]["type"] == "802-3-ethernet":
self.settings = settings_update(
MINIMAL_ETHERNET_SETTINGS_FIXTURE, properties
)
else:
self.settings = settings_update(MINIMAL_SETTINGS_FIXTURE, properties)
# Post process addresses/address-data and dns/dns-data
# If both "address" and "address-data" are provided the former wins
# If both "dns" and "dns-data" are provided the former wins
if "ipv4" in properties:
ipv4 = properties["ipv4"]
if "address-data" in ipv4:
addresses = Variant("aau", [])
for entry in ipv4["address-data"].value:
addresses.value.append(
[
socket.htonl(int(IPv4Address(entry["address"].value))),
entry["prefix"].value,
0,
]
)
self.settings["ipv4"]["addresses"] = addresses
if "addresses" in ipv4:
address_data = Variant("aa{sv}", [])
for entry in ipv4["addresses"].value:
ipv4address = IPv4Address(socket.ntohl(entry[0]))
address_data.value.append(
{
"address": Variant("s", str(ipv4address)),
"prefix": Variant("u", int(entry[1])),
}
)
self.settings["ipv4"]["address-data"] = address_data
if "dns-data" in ipv4:
dns = Variant("au", [])
for entry in ipv4["dns-data"].value:
dns.value.append(socket.htonl(int(IPv4Address(entry))))
self.settings["ipv4"]["dns"] = dns
if "dns" in ipv4:
dns_data = Variant("as", [])
for entry in ipv4["dns"].value:
dns_data.value.append(str(IPv4Address(socket.ntohl(entry))))
self.settings["ipv4"]["dns-data"] = dns_data
self.Updated() self.Updated()
@dbus_method() @dbus_method()

View File

@ -9,7 +9,9 @@ from dbus_fast.service import PropertyAccess, dbus_property, signal
from .base import DBusServiceMock, dbus_method from .base import DBusServiceMock, dbus_method
BUS_NAME = "org.freedesktop.NetworkManager" BUS_NAME = "org.freedesktop.NetworkManager"
DEFAULT_OBJECT_PATH = "/org/freedesktop/NetworkManager/Devices/1" ETHERNET_DEVICE_OBJECT_PATH = "/org/freedesktop/NetworkManager/Devices/1"
WIRELESS_DEVICE_OBJECT_PATH = "/org/freedesktop/NetworkManager/Devices/3"
DEFAULT_OBJECT_PATH = ETHERNET_DEVICE_OBJECT_PATH
def setup(object_path: str | None = None) -> DBusServiceMock: def setup(object_path: str | None = None) -> DBusServiceMock:
@ -112,7 +114,7 @@ FIXTURES: dict[str, DeviceFixture] = {
FirmwareMissing=False, FirmwareMissing=False,
NmPluginMissing=False, NmPluginMissing=False,
DeviceType=2, DeviceType=2,
AvailableConnections=[], AvailableConnections=["/org/freedesktop/NetworkManager/Settings/3"],
PhysicalPortId="", PhysicalPortId="",
Mtu=1500, Mtu=1500,
Metered=0, Metered=0,

View File

@ -19,7 +19,7 @@ from tests.dbus_service_mocks.network_active_connection import (
ActiveConnection as ActiveConnectionService, ActiveConnection as ActiveConnectionService,
) )
from tests.dbus_service_mocks.network_connection_settings import ( from tests.dbus_service_mocks.network_connection_settings import (
SETTINGS_FIXTURE, SETTINGS_1_FIXTURE,
ConnectionSettings as ConnectionSettingsService, ConnectionSettings as ConnectionSettingsService,
) )
from tests.dbus_service_mocks.network_device_wireless import ( from tests.dbus_service_mocks.network_device_wireless import (
@ -83,11 +83,15 @@ async def test_load(
assert name_dict["wlan0"].enabled is False assert name_dict["wlan0"].enabled is False
assert connection_settings_service.settings["ipv4"]["method"].value == "auto" assert connection_settings_service.settings["ipv4"]["method"].value == "auto"
assert "address-data" not in connection_settings_service.settings["ipv4"] assert connection_settings_service.settings["ipv4"]["address-data"] == Variant(
"aa{sv}", []
)
assert "gateway" not in connection_settings_service.settings["ipv4"] assert "gateway" not in connection_settings_service.settings["ipv4"]
assert "dns" not in connection_settings_service.settings["ipv4"] assert "dns" not in connection_settings_service.settings["ipv4"]
assert connection_settings_service.settings["ipv6"]["method"].value == "auto" assert connection_settings_service.settings["ipv6"]["method"].value == "auto"
assert "address-data" not in connection_settings_service.settings["ipv6"] assert connection_settings_service.settings["ipv6"]["address-data"] == Variant(
"aa{sv}", []
)
assert "gateway" not in connection_settings_service.settings["ipv6"] assert "gateway" not in connection_settings_service.settings["ipv6"]
assert "dns" not in connection_settings_service.settings["ipv6"] assert "dns" not in connection_settings_service.settings["ipv6"]
@ -110,7 +114,7 @@ async def test_load_with_disabled_methods(
network_manager_service.ActivateConnection.calls.clear() network_manager_service.ActivateConnection.calls.clear()
disabled = {"method": Variant("s", "disabled")} disabled = {"method": Variant("s", "disabled")}
connection_settings_service.settings = SETTINGS_FIXTURE | { connection_settings_service.settings = SETTINGS_1_FIXTURE | {
"ipv4": disabled, "ipv4": disabled,
"ipv6": disabled, "ipv6": disabled,
} }
@ -119,15 +123,6 @@ async def test_load_with_disabled_methods(
await coresys.host.network.load() await coresys.host.network.load()
assert network_manager_service.ActivateConnection.calls == [] assert network_manager_service.ActivateConnection.calls == []
assert connection_settings_service.settings["ipv4"]["method"].value == "disabled"
assert "address-data" not in connection_settings_service.settings["ipv4"]
assert "gateway" not in connection_settings_service.settings["ipv4"]
assert "dns" not in connection_settings_service.settings["ipv4"]
assert connection_settings_service.settings["ipv6"]["method"].value == "disabled"
assert "address-data" not in connection_settings_service.settings["ipv6"]
assert "gateway" not in connection_settings_service.settings["ipv6"]
assert "dns" not in connection_settings_service.settings["ipv6"]
async def test_load_with_network_connection_issues( async def test_load_with_network_connection_issues(
coresys: CoreSys, coresys: CoreSys,