- hypr/apps.lua - hypr/autostart.lua - hypr/envs.lua - hypr/hyprland.lua - hypr/hyprsunset.conf - hypr/input.lua - hypr/looknfeel.lua - hypr/omasettings.lua - hypr/xdph.conf - omarchy/branding/about.txt - omarchy/branding/screensaver.txt - omarchy/extensions/omarchy-menu.jsonc - omarchy/hooks/battery-low.d/play-warning-sound.sample - omarchy/hooks/font-set.d/show-font-notification.sample - omarchy/hooks/post-boot.d/weather.sample - omarchy/hooks/post-update.d/install-voxtype.hook - omarchy/hooks/post-update.d/setup-agent.hook - omarchy/hooks/post-update.d/setup-fingerprint.hook - omarchy/hooks/post-update.d/show-update-notification.sample - omarchy/hooks/pre-refresh-pacman.d/add-custom-repo.sample - omarchy/hooks/theme-set.d/show-theme-notification.sample - omarchy/shell.json - omarchy/shell.toml - omarchy/theme.name - omarchy/themes/azure-glow/README.md - omarchy/themes/azure-glow/alacritty.toml - omarchy/themes/azure-glow/btop.theme - omarchy/themes/azure-glow/hyprland.conf - omarchy/themes/azure-glow/hyprlock.conf - omarchy/themes/azure-glow/icons.theme - … 269 more
168 lines
7.2 KiB
Python
168 lines
7.2 KiB
Python
"""Tests for net.py — iw parsing, the trace verdict, tethering, the connection-name cache.
|
|
|
|
Run: python3 -m unittest discover -s test
|
|
"""
|
|
|
|
import sys
|
|
import unittest
|
|
from pathlib import Path
|
|
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
|
|
|
from nexthopd import net # noqa: E402
|
|
from nexthopd.net import nm_metered, tether_from_gateway # noqa: E402
|
|
from support import FIXTURES # noqa: E402
|
|
|
|
|
|
class IwParsing(unittest.TestCase):
|
|
def test_link_fixture(self):
|
|
raw = (FIXTURES / "iw-link.txt").read_text()
|
|
original = net._run
|
|
net._run = lambda cmd, timeout=2.0: raw
|
|
try:
|
|
info = net.wifi_link("wlo1")
|
|
finally:
|
|
net._run = original
|
|
self.assertEqual(info["ssid"], "Excitel")
|
|
self.assertEqual(info["freq_mhz"], 5180) # float in fixture, int out
|
|
self.assertEqual(info["signal_dbm"], -64)
|
|
self.assertEqual(info["band"], "5 GHz")
|
|
self.assertEqual(info["channel"], 36)
|
|
self.assertEqual(info["standard"], "802.11ax")
|
|
self.assertEqual(info["width_mhz"], 40)
|
|
|
|
def test_channel_map(self):
|
|
self.assertEqual(net._freq_to_channel(2412), 1)
|
|
self.assertEqual(net._freq_to_channel(2484), 14)
|
|
self.assertEqual(net._freq_to_channel(5180), 36)
|
|
self.assertEqual(net._freq_to_channel(5955), 1)
|
|
|
|
|
|
class TraceParsing(unittest.TestCase):
|
|
"""The cdn-cgi/trace response yields one validated address or nothing."""
|
|
|
|
def test_recorded_response(self):
|
|
# Recorded from a real fetch of speed.cloudflare.com/cdn-cgi/trace
|
|
# (address substituted): sixteen key=value lines, ip= among them.
|
|
text = (FIXTURES / "cf-trace.txt").read_text()
|
|
# The recorded fixture carries loc=XX (Cloudflare's "unknown"), so
|
|
# no country survives; its colo passes through validated.
|
|
self.assertEqual(net.parse_trace(text),
|
|
{"ip": "198.51.100.7", "family": "v4",
|
|
"edge": "XXX"})
|
|
|
|
def test_country_and_edge_come_free_with_the_address(self):
|
|
# Both are already in the response the reachability check fetches.
|
|
self.assertEqual(
|
|
net.parse_trace("ip=1.2.3.4\nloc=IN\ncolo=DEL\n"),
|
|
{"ip": "1.2.3.4", "family": "v4",
|
|
"country": "IN", "edge": "DEL"})
|
|
|
|
def test_unknown_country_is_withheld_not_shown(self):
|
|
# Cloudflare answers XX when it does not know. Showing a country
|
|
# called XX would be inventing one.
|
|
got = net.parse_trace("ip=1.2.3.4\nloc=XX\ncolo=DEL\n")
|
|
self.assertNotIn("country", got)
|
|
self.assertEqual(got["edge"], "DEL")
|
|
|
|
def test_only_a_country_shaped_country_gets_out(self):
|
|
# Nothing free-form from the wire reaches the shell, same rule as
|
|
# the address itself.
|
|
for bad in ("in", "IND", "I", "I1", "<b>", "\u00cd\u00d1"):
|
|
got = net.parse_trace("ip=1.2.3.4\nloc=%s\n" % bad)
|
|
self.assertNotIn("country", got, bad)
|
|
for bad in ("del", "D", "TOOLONG", "D3L", "<i>"):
|
|
got = net.parse_trace("ip=1.2.3.4\ncolo=%s\n" % bad)
|
|
self.assertNotIn("edge", got, bad)
|
|
|
|
def test_country_and_edge_never_stand_in_for_an_address(self):
|
|
# The address is the point; decoration alone is not a result.
|
|
self.assertIsNone(net.parse_trace("loc=IN\ncolo=DEL\n"))
|
|
self.assertIsNone(net.parse_trace("ip=nope\nloc=IN\ncolo=DEL\n"))
|
|
|
|
def test_v6_is_labelled(self):
|
|
self.assertEqual(net.parse_trace("h=x\nip=2001:db8::7\nts=1\n"),
|
|
{"ip": "2001:db8::7", "family": "v6"})
|
|
|
|
def test_only_a_real_address_gets_out(self):
|
|
# Whatever else the response holds must never reach the shell.
|
|
self.assertIsNone(net.parse_trace("ip=<b>not-an-ip</b>\n"))
|
|
self.assertIsNone(net.parse_trace("ip=1.2.3.4.5\n"))
|
|
self.assertIsNone(net.parse_trace("h=x\nts=1\n"))
|
|
self.assertIsNone(net.parse_trace(""))
|
|
|
|
def test_input_is_bounded_before_parsing(self):
|
|
# An ip= line beyond the size cap is as good as absent.
|
|
self.assertIsNone(net.parse_trace("x=" + "a" * 5000 + "\nip=1.2.3.4\n"))
|
|
self.assertIsNone(net.parse_trace("k=v\n" * 100 + "ip=1.2.3.4\n"))
|
|
|
|
|
|
class TetherDetection(unittest.TestCase):
|
|
"""A phone sharing its data, from the one signal that is reliable."""
|
|
|
|
def test_the_documented_ranges(self):
|
|
self.assertEqual(tether_from_gateway("172.20.10.1"),
|
|
{"kind": "ios", "label": "iPhone"})
|
|
self.assertEqual(tether_from_gateway("192.168.43.1")["kind"], "android")
|
|
self.assertEqual(tether_from_gateway("192.168.42.129")["kind"],
|
|
"android")
|
|
self.assertEqual(tether_from_gateway("192.168.137.1")["kind"],
|
|
"windows")
|
|
|
|
def test_ordinary_gateways_are_not_phones(self):
|
|
# Including the hotel gateway that started this: 172.20.0.1 is close
|
|
# to the iOS range and outside it, so the /28 matters.
|
|
for gw in ("192.168.1.1", "172.20.0.1", "10.0.0.1", "172.20.11.1"):
|
|
self.assertIsNone(tether_from_gateway(gw), gw)
|
|
|
|
def test_nothing_and_nonsense_are_not_phones(self):
|
|
for gw in ("", None, "not-an-ip", "999.1.1.1"):
|
|
self.assertIsNone(tether_from_gateway(gw))
|
|
|
|
def test_ipv6_gateway_does_not_raise(self):
|
|
self.assertIsNone(tether_from_gateway("fe80::1"))
|
|
|
|
def test_a_guess_from_networkmanager_is_not_evidence(self):
|
|
# A live iPhone hotspot reports "no (guessed)", so only an explicit
|
|
# answer may count — and a guessed YES must not either.
|
|
import nexthopd.net as netmod
|
|
original = netmod._run
|
|
try:
|
|
for raw, expected in (
|
|
("GENERAL.METERED:yes", True),
|
|
("GENERAL.METERED:yes (guessed)", False),
|
|
("GENERAL.METERED:no", False),
|
|
("GENERAL.METERED:no (guessed)", False),
|
|
("", False),
|
|
(None, False),
|
|
):
|
|
netmod._run = lambda *a, **k: raw
|
|
self.assertEqual(nm_metered("wlan0"), expected, raw)
|
|
finally:
|
|
netmod._run = original
|
|
|
|
|
|
class ConnectionNameCache(unittest.TestCase):
|
|
def test_nmcli_is_asked_on_a_new_key_or_after_the_ttl_only(self):
|
|
calls = []
|
|
orig = net.connection_name
|
|
net.connection_name = lambda iface: calls.append(iface) or "Home"
|
|
net._name_cache.clear()
|
|
self.addCleanup(setattr, net, "connection_name", orig)
|
|
self.addCleanup(net._name_cache.clear)
|
|
key = ("wlo1", "192.168.1.1", "aa:bb")
|
|
self.assertEqual(net.connection_name_cached("wlo1", key, now=100), "Home")
|
|
self.assertEqual(net.connection_name_cached("wlo1", key, now=130), "Home")
|
|
self.assertEqual(len(calls), 1)
|
|
# A new gateway or BSSID is a new network: ask again.
|
|
net.connection_name_cached("wlo1", ("wlo1", "10.0.0.1", "aa:bb"), now=131)
|
|
self.assertEqual(len(calls), 2)
|
|
# ...and so is a rename the user made, eventually.
|
|
net.connection_name_cached("wlo1", ("wlo1", "10.0.0.1", "aa:bb"),
|
|
now=131 + net.NAME_CACHE_TTL_S + 1)
|
|
self.assertEqual(len(calls), 3)
|
|
|
|
|
|
if __name__ == "__main__":
|
|
unittest.main()
|