test_network_utils.py 3.5 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283
  1. """Tests for network interface enumeration.
  2. Focus: the platform routing in get_network_interfaces(). macOS/BSD have fcntl
  3. but not the Linux SIOCGIFADDR/SIOCGIFNETMASK ioctls, so the ioctl path there
  4. silently returns nothing and the VP bind-interface dropdown comes up empty.
  5. Everything that isn't Linux must go through the cross-platform psutil path.
  6. """
  7. import socket
  8. from collections import namedtuple
  9. from unittest.mock import patch
  10. from backend.app.services import network_utils
  11. # Mimic the shape of psutil.net_if_addrs() / net_if_stats() entries we read.
  12. _Addr = namedtuple("snicaddr", ["family", "address", "netmask", "broadcast", "ptp"])
  13. _Stats = namedtuple("snicstats", ["isup", "duplex", "speed", "mtu", "flags"])
  14. def _fake_psutil():
  15. addrs = {
  16. "en0": [_Addr(socket.AF_INET, "192.168.1.50", "255.255.255.0", None, None)],
  17. "lo0": [_Addr(socket.AF_INET, "127.0.0.1", "255.0.0.0", None, None)],
  18. "awdl0": [_Addr(socket.AF_INET, "169.254.10.20", "255.255.0.0", None, None)],
  19. "utun3": [_Addr(socket.AF_INET, "100.64.0.7", "255.255.255.255", None, None)],
  20. "en5": [_Addr(socket.AF_INET, "10.0.0.9", "255.255.255.0", None, None)],
  21. }
  22. stats = {
  23. "en0": _Stats(True, 0, 0, 1500, 0),
  24. "lo0": _Stats(True, 0, 0, 16384, 0),
  25. "awdl0": _Stats(True, 0, 0, 1500, 0),
  26. "utun3": _Stats(True, 0, 0, 1500, 0),
  27. "en5": _Stats(False, 0, 0, 1500, 0), # down → skipped
  28. }
  29. return addrs, stats
  30. @patch("backend.app.services.network_utils.sys")
  31. def test_macos_routes_to_psutil(mock_sys):
  32. """On darwin, get_network_interfaces() must use psutil, not the ioctl path."""
  33. mock_sys.platform = "darwin"
  34. with patch.object(network_utils, "_get_network_interfaces_psutil", return_value=[{"name": "en0"}]) as psutil_path:
  35. result = network_utils.get_network_interfaces()
  36. psutil_path.assert_called_once()
  37. assert result == [{"name": "en0"}]
  38. @patch("backend.app.services.network_utils.sys")
  39. def test_windows_routes_to_psutil(mock_sys):
  40. mock_sys.platform = "win32"
  41. with patch.object(network_utils, "_get_network_interfaces_psutil", return_value=[]) as psutil_path:
  42. network_utils.get_network_interfaces()
  43. psutil_path.assert_called_once()
  44. @patch("backend.app.services.network_utils.sys")
  45. def test_linux_does_not_use_psutil(mock_sys):
  46. """Linux keeps the ioctl path — psutil helper must not be invoked."""
  47. mock_sys.platform = "linux"
  48. with patch.object(network_utils, "_get_network_interfaces_psutil") as psutil_path:
  49. # The ioctl path runs for real here; we only assert it wasn't short-circuited
  50. # to psutil. Its actual return depends on the host, so we don't assert on it.
  51. network_utils.get_network_interfaces()
  52. psutil_path.assert_not_called()
  53. def test_psutil_path_filters_and_returns_bindable_ips():
  54. """The psutil path drops loopback/link-local/down ifaces, keeps real + VPN ones."""
  55. addrs, stats = _fake_psutil()
  56. with (
  57. patch("psutil.net_if_addrs", return_value=addrs),
  58. patch("psutil.net_if_stats", return_value=stats),
  59. ):
  60. result = network_utils._get_network_interfaces_psutil()
  61. by_name = {i["name"]: i for i in result}
  62. assert "en0" in by_name # normal LAN interface
  63. assert by_name["en0"]["ip"] == "192.168.1.50"
  64. assert by_name["en0"]["subnet"] == "192.168.1.0/24"
  65. assert "utun3" in by_name # Tailscale/VPN — legitimately bindable
  66. assert "lo0" not in by_name # loopback filtered
  67. assert "awdl0" not in by_name # link-local (169.254) filtered
  68. assert "en5" not in by_name # interface down, skipped