bambuddy/backend/tests/unit/services/test_network_utils.py
maziggy af867c0392
Some checks failed
Security Audit / Python Security Analysis (Bandit) (push) Failing after 47s
Security Audit / Container Security Scan (Trivy) (push) Failing after 46s
Security Audit / Backend Security Audit (push) Failing after 46s
Security Audit / Frontend Security Audit (push) Failing after 6s
fix(vp): populate bind-interface list on macOS (route non-Linux to psutil)
get_network_interfaces() only sent Windows to the psutil path; macOS fell into
the Linux ioctl branch, whose SIOCGIFADDR/SIOCGIFNETMASK ioctls are Linux-only.
macOS/BSD have fcntl but different ioctl numbers, so every call raised OSError
and the function returned an empty list — the VP bind-interface dropdown showed
nothing. Route all non-Linux platforms through the cross-platform psutil path.
2026-07-06 13:03:22 +02:00

83 lines
3.5 KiB
Python

"""Tests for network interface enumeration.
Focus: the platform routing in get_network_interfaces(). macOS/BSD have fcntl
but not the Linux SIOCGIFADDR/SIOCGIFNETMASK ioctls, so the ioctl path there
silently returns nothing and the VP bind-interface dropdown comes up empty.
Everything that isn't Linux must go through the cross-platform psutil path.
"""
import socket
from collections import namedtuple
from unittest.mock import patch
from backend.app.services import network_utils
# Mimic the shape of psutil.net_if_addrs() / net_if_stats() entries we read.
_Addr = namedtuple("snicaddr", ["family", "address", "netmask", "broadcast", "ptp"])
_Stats = namedtuple("snicstats", ["isup", "duplex", "speed", "mtu", "flags"])
def _fake_psutil():
addrs = {
"en0": [_Addr(socket.AF_INET, "192.168.1.50", "255.255.255.0", None, None)],
"lo0": [_Addr(socket.AF_INET, "127.0.0.1", "255.0.0.0", None, None)],
"awdl0": [_Addr(socket.AF_INET, "169.254.10.20", "255.255.0.0", None, None)],
"utun3": [_Addr(socket.AF_INET, "100.64.0.7", "255.255.255.255", None, None)],
"en5": [_Addr(socket.AF_INET, "10.0.0.9", "255.255.255.0", None, None)],
}
stats = {
"en0": _Stats(True, 0, 0, 1500, 0),
"lo0": _Stats(True, 0, 0, 16384, 0),
"awdl0": _Stats(True, 0, 0, 1500, 0),
"utun3": _Stats(True, 0, 0, 1500, 0),
"en5": _Stats(False, 0, 0, 1500, 0), # down → skipped
}
return addrs, stats
@patch("backend.app.services.network_utils.sys")
def test_macos_routes_to_psutil(mock_sys):
"""On darwin, get_network_interfaces() must use psutil, not the ioctl path."""
mock_sys.platform = "darwin"
with patch.object(network_utils, "_get_network_interfaces_psutil", return_value=[{"name": "en0"}]) as psutil_path:
result = network_utils.get_network_interfaces()
psutil_path.assert_called_once()
assert result == [{"name": "en0"}]
@patch("backend.app.services.network_utils.sys")
def test_windows_routes_to_psutil(mock_sys):
mock_sys.platform = "win32"
with patch.object(network_utils, "_get_network_interfaces_psutil", return_value=[]) as psutil_path:
network_utils.get_network_interfaces()
psutil_path.assert_called_once()
@patch("backend.app.services.network_utils.sys")
def test_linux_does_not_use_psutil(mock_sys):
"""Linux keeps the ioctl path — psutil helper must not be invoked."""
mock_sys.platform = "linux"
with patch.object(network_utils, "_get_network_interfaces_psutil") as psutil_path:
# The ioctl path runs for real here; we only assert it wasn't short-circuited
# to psutil. Its actual return depends on the host, so we don't assert on it.
network_utils.get_network_interfaces()
psutil_path.assert_not_called()
def test_psutil_path_filters_and_returns_bindable_ips():
"""The psutil path drops loopback/link-local/down ifaces, keeps real + VPN ones."""
addrs, stats = _fake_psutil()
with (
patch("psutil.net_if_addrs", return_value=addrs),
patch("psutil.net_if_stats", return_value=stats),
):
result = network_utils._get_network_interfaces_psutil()
by_name = {i["name"]: i for i in result}
assert "en0" in by_name # normal LAN interface
assert by_name["en0"]["ip"] == "192.168.1.50"
assert by_name["en0"]["subnet"] == "192.168.1.0/24"
assert "utun3" in by_name # Tailscale/VPN — legitimately bindable
assert "lo0" not in by_name # loopback filtered
assert "awdl0" not in by_name # link-local (169.254) filtered
assert "en5" not in by_name # interface down, skipped