server-up/tests/test_network_detect.py
Ramon 4346fefe4d
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m10s
v0.5.41-beta - netwerk toevoegen liep vast tijdens het typen
JSON-parsefout in de browser bij het toevoegen van een netwerk.

De live beoordeling die bij elke toetsaanslag afgaat, bouwde een lijst van alle
bruikbare adressen in het bereik. Tijdens het intypen van 192.168.1.0/24 is de
tussenstand 192.168.1.0/2 een geldig netwerk met 1.073.741.824 adressen; het
verzoek liep daarop vast en de browser kreeg geen JSON terug.

- review() rekent eerste adres, laatste adres en aantal nu uit via
  num_addresses in plaats van hosts() te doorlopen. Alle prefixes van /1 tot
  /32 antwoorden binnen 0,05 seconde.
- suggest_range() had hetzelfde probleem: die somde alle /28-blokken op, bij
  een /8 ruim een miljoen. Rekent nu van achteren naar voren, hoogstens
  zestien stappen.
- Waarschuwing bij meer dan 4096 adressen in het bereik; dat is vrijwel altijd
  een typefout in het prefix.
- Tests met /1 t/m /32, inclusief een vergelijking van _bruikbaar_bereik() met
  de uitkomst van hosts() uit de stdlib, en tijdslimieten zodat een
  herintroductie meteen opvalt.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-26 23:42:56 +02:00

234 lines
9.1 KiB
Python

"""Netwerkdetectie: /proc/net/route parsen, ranges voorstellen en beoordelen."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
from core import networks
# Zoals /proc/net/route eruitziet op een host met één LAN-interface.
# Adressen staan als little-endian hex: 0100A8C0 = 192.168.0.1
ROUTE_ENKEL = """Iface\tDestination\tGateway \tFlags\tRefCnt\tUse\tMetric\tMask\t\tMTU\tWindow\tIRTT
eth0\t00000000\t0101A8C0\t0003\t0\t0\t100\t00000000\t0\t0\t0
eth0\t0001A8C0\t00000000\t0001\t0\t0\t100\t00FFFFFF\t0\t0\t0
docker0\t000011AC\t00000000\t0001\t0\t0\t0\t0000FFFF\t0\t0\t0
"""
def test_parse_vindt_lan_interface():
n = networks.parse_proc_route(ROUTE_ENKEL)
assert len(n) == 1
assert n[0] == {"interface": "eth0", "subnet": "192.168.1.0/24",
"gateway": "192.168.1.1"}
def test_parse_negeert_interfaces_zonder_default_route():
"""docker0 heeft wel een subnet maar geen default route — geen LAN dus."""
assert all(x["interface"] != "docker0"
for x in networks.parse_proc_route(ROUTE_ENKEL))
def test_parse_meerdere_interfaces():
tekst = ROUTE_ENKEL + "eth1\t00000000\t0102A8C0\t0003\t0\t0\t200\t00000000\t0\t0\t0\n" \
"eth1\t0002A8C0\t00000000\t0001\t0\t0\t200\t00FFFFFF\t0\t0\t0\n"
n = networks.parse_proc_route(tekst)
assert [x["interface"] for x in n] == ["eth0", "eth1"]
assert n[1]["subnet"] == "192.168.2.0/24"
def test_parse_lege_of_onzinnige_invoer():
assert networks.parse_proc_route("") == []
assert networks.parse_proc_route("kop\nonzin regel\n") == []
# ── Voorgestelde range ───────────────────────────────────────────────────────
@pytest.mark.parametrize("subnet,gateway,verwacht", [
("192.168.1.0/24", "192.168.1.1", "192.168.1.240/28"),
("10.0.0.0/24", "10.0.0.1", "10.0.0.240/28"),
("192.168.0.0/16", "192.168.0.1", "192.168.255.240/28"),
])
def test_suggest_range_pakt_het_einde_van_het_subnet(subnet, gateway, verwacht):
assert networks.suggest_range(subnet, gateway) == verwacht
def test_suggest_range_vermijdt_de_gateway():
"""Een gateway aan het eind van het subnet mag niet in de range vallen."""
r = networks.suggest_range("192.168.1.0/24", "192.168.1.250")
import ipaddress
assert ipaddress.ip_address("192.168.1.250") not in ipaddress.ip_network(r)
def test_suggest_range_bij_klein_subnet():
assert networks.suggest_range("192.168.1.0/28", "192.168.1.1") == ""
def test_suggest_range_bij_onzin():
assert networks.suggest_range("geen-subnet") == ""
# ── Beoordeling ──────────────────────────────────────────────────────────────
BASIS = {"name": "lan", "driver": "macvlan", "parent": "eth0",
"subnet": "192.168.1.0/24", "gateway": "192.168.1.1",
"ip_range": "192.168.1.240/28"}
def test_review_goede_definitie():
r = networks.review(BASIS)
assert r["ok"] is True
assert r["errors"] == []
assert r["info"]["count"] == 14 # .241 t/m .254
assert r["info"]["first"] == "192.168.1.241"
assert r["info"]["last"] == "192.168.1.254"
def test_review_waarschuwt_bij_gateway_binnen_een_expliciete_range():
r = networks.review({**BASIS, "ip_range": "192.168.1.0/25"})
assert r["ok"] is True # Docker houdt het adres vrij, dus geen fout
assert any("gateway" in w for w in r["warnings"])
def test_review_ziet_gateway_in_het_subnet_niet_als_probleem():
"""Zonder eigen range is 'gateway zit in het subnet' de normale situatie."""
r = networks.review({**BASIS, "ip_range": ""})
assert not any("gateway" in w.lower() for w in r["warnings"])
def test_review_waarschuwt_zonder_range():
r = networks.review({**BASIS, "ip_range": ""})
assert r["ok"] is True
assert any("DHCP" in w for w in r["warnings"])
def test_review_waarschuwt_zonder_gateway():
r = networks.review({**BASIS, "gateway": ""})
assert r["ok"] is True
assert any("internet" in w for w in r["warnings"])
def test_review_waarschuwt_bij_erg_kleine_range():
r = networks.review({**BASIS, "ip_range": "192.168.1.252/30"})
assert r["ok"] is True
assert any("adressen" in w for w in r["warnings"])
def test_review_loopt_niet_vast_op_een_enorm_bereik():
"""Tijdens het typen van '192.168.1.0/24' is '192.168.1.0/2' een geldig
netwerk met ruim een miljard adressen. Dat mag geen lijst worden: het
verzoek liep vast en de browser kreeg geen JSON meer terug."""
import time
start = time.time()
r = networks.review({**BASIS, "subnet": "192.168.1.0/2", "ip_range": ""})
duur = time.time() - start
assert duur < 1.0, f"duurde {duur:.1f}s — bouwt vermoedelijk weer een lijst"
# 2^30 adressen, min netwerk- en broadcastadres, min de gateway die er
# toevallig binnen valt.
assert r["info"]["count"] == 1073741821
@pytest.mark.parametrize("prefix", ["1", "2", "8", "12", "16"])
def test_review_is_snel_bij_elk_prefix(prefix):
"""Elke tussenstand tijdens het typen moet direct antwoord geven."""
import time
start = time.time()
networks.review({**BASIS, "subnet": f"10.0.0.0/{prefix}", "ip_range": ""})
assert time.time() - start < 1.0
def test_suggest_range_is_snel_bij_een_groot_subnet():
import time
start = time.time()
r = networks.suggest_range("10.0.0.0/8", "10.0.0.1")
assert time.time() - start < 1.0
assert r == "10.255.255.240/28"
def test_review_waarschuwt_bij_een_verdacht_groot_bereik():
r = networks.review({**BASIS, "subnet": "10.0.0.0/8",
"gateway": "10.0.0.1", "ip_range": ""})
assert any("prefix" in w for w in r["warnings"])
@pytest.mark.parametrize("bereik,eerste,laatste,aantal", [
("192.168.1.0/24", "192.168.1.1", "192.168.1.254", 254),
("192.168.1.240/28", "192.168.1.241", "192.168.1.254", 14),
("192.168.1.0/30", "192.168.1.1", "192.168.1.2", 2),
("192.168.1.0/31", "192.168.1.0", "192.168.1.1", 2),
("192.168.1.5/32", "192.168.1.5", "192.168.1.5", 1),
])
def test_bruikbaar_bereik_rekent_goed(bereik, eerste, laatste, aantal):
"""Zelfde uitkomst als hosts() zou geven, maar zonder die te doorlopen."""
import ipaddress
net = ipaddress.ip_network(bereik)
e, l, n = networks._bruikbaar_bereik(net)
assert (str(e), str(l), n) == (eerste, laatste, aantal)
assert n == len(list(net.hosts())) # controle tegen de stdlib
def test_bruikbaar_bereik_trekt_de_gateway_eraf():
import ipaddress
net = ipaddress.ip_network("192.168.1.0/24")
e, l, n = networks._bruikbaar_bereik(net, "192.168.1.1")
assert (str(e), n) == ("192.168.1.2", 253)
e, l, n = networks._bruikbaar_bereik(net, "192.168.1.254")
assert (str(l), n) == ("192.168.1.253", 253)
# Gateway in het midden verschuift de randen niet.
e, l, n = networks._bruikbaar_bereik(net, "192.168.1.100")
assert (str(e), str(l), n) == ("192.168.1.1", "192.168.1.254", 253)
def test_bruikbaar_bereik_negeert_gateway_buiten_het_bereik():
import ipaddress
net = ipaddress.ip_network("192.168.1.240/28")
assert networks._bruikbaar_bereik(net, "192.168.1.1")[2] == 14
def test_review_geeft_blokkerende_validatiefout_door():
r = networks.review({**BASIS, "subnet": "onzin"})
assert r["ok"] is False
assert any("subnet" in e.lower() for e in r["errors"])
def test_review_range_buiten_subnet():
r = networks.review({**BASIS, "ip_range": "10.9.9.0/28"})
assert r["ok"] is False
# ── Endpoints ────────────────────────────────────────────────────────────────
def test_review_endpoint(client):
from conftest import login
csrf = login(client)
r = client.post("/api/networks/review", json=BASIS,
headers={"X-CSRF-Token": csrf})
d = r.get_json()
assert d["ok"] is True
assert d["info"]["count"] == 14
def test_networks_endpoint_geeft_detectie_terug(client, monkeypatch):
from conftest import login
from core import networks as nw
login(client)
monkeypatch.setattr(nw, "detect_host_networks", lambda force=False: {
"networks": [{"interface": "eth0", "subnet": "192.168.1.0/24",
"gateway": "192.168.1.1", "suggested_range": "192.168.1.240/28",
"driver": "macvlan", "name": "lan"}],
"error": ""})
d = client.get("/api/networks").get_json()
assert d["detected"][0]["interface"] == "eth0"
assert d["interfaces"] == ["eth0"]
def test_detectiefout_wordt_doorgegeven(client, monkeypatch):
from conftest import login
from core import networks as nw
login(client)
monkeypatch.setattr(nw, "detect_host_networks", lambda force=False: {
"networks": [], "error": "docker niet bereikbaar"})
d = client.get("/api/networks").get_json()
assert d["detect_error"] == "docker niet bereikbaar"
assert d["detected"] == []