"""Neue Geräte im Heimnetz (Ausbauplan „Später“, Punkt 21, 26.09.2026): Der Ausführer liest die Nachbartabelle des Proxmox-Hosts, der Homelab-Teil führt ein Register und meldet Neues einmal; der Wächter zeigt es, bis „Ist bekannt“.""" import importlib.util import json from pathlib import Path import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from kern import einstellungen as einstellungen_mod from services.homelab import netzgeraete AUSFUEHRER = Path(__file__).resolve().parents[2] / "deploy" / "homelab" / "ausfuehrer.py" T0 = 1_790_400_000.0 @pytest.fixture(scope="module") def a(): spec = importlib.util.spec_from_file_location("ausfuehrer_netz", AUSFUEHRER) modul = importlib.util.module_from_spec(spec) spec.loader.exec_module(modul) return modul def test_netz_der_bruecke(a): addr = json.dumps([{"ifname": "vmbr0", "addr_info": [{"family": "inet", "local": "192.168.178.108", "prefixlen": 24}]}]) assert str(a.netz_der_bruecke(addr)) == "192.168.178.0/24" gross = json.dumps([{"addr_info": [{"family": "inet", "local": "10.0.0.5", "prefixlen": 16}]}]) assert a.netz_der_bruecke(gross) is None # größer als /24 wird nicht abgesucht assert a.netz_der_bruecke("kaputt") is None def test_nachbarn(a): import ipaddress neigh = json.dumps([ {"dst": "192.168.178.1", "lladdr": "74:42:7F:00:00:01", "state": ["REACHABLE"]}, {"dst": "192.168.178.23", "lladdr": "6e:81:03:00:00:02", "state": ["STALE"]}, {"dst": "192.168.178.40", "state": ["FAILED"]}, {"dst": "192.168.178.41", "lladdr": "aa:bb:cc:00:00:03", "state": ["INCOMPLETE"]}, {"dst": "fe80::1", "lladdr": "74:42:7f:00:00:01", "state": ["REACHABLE"]}, {"dst": "10.1.1.1", "lladdr": "11:22:33:00:00:04", "state": ["REACHABLE"]}, ]) assert a.nachbarn(neigh, ipaddress.ip_network("192.168.178.0/24")) == [ {"ip": "192.168.178.1", "mac": "74:42:7f:00:00:01"}, {"ip": "192.168.178.23", "mac": "6e:81:03:00:00:02"}] @pytest.fixture def daten(tmp_path, monkeypatch): monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) einstellungen_mod.einstellungen.cache_clear() gemeldet: list[str] = [] monkeypatch.setattr(netzgeraete.announce, "notify_telegram", lambda betreff, text, **k: gemeldet.append(text)) yield gemeldet einstellungen_mod.einstellungen.cache_clear() def _bericht(*geraete: tuple[str, str, str | None], gaeste: tuple[str, ...] = ()) -> dict: return {"host": {"netz": {"zeit": T0, "netz": "192.168.178.0/24", "geraete": [{"ip": ip, "mac": mac, "name": name} for ip, mac, name in geraete]}}, "gaeste": [{"vmid": 100 + i, "ip": ip} for i, ip in enumerate(gaeste)]} FRITZ = ("192.168.178.1", "74:42:7f:00:00:01", "fritz.box") PC = ("192.168.178.22", "34:5a:60:00:00:05", "TobisNicerPC.fritz.box") def test_erstes_mal_ist_alles_bekannt_dann_meldet_es_neues(daten): assert netzgeraete.abgleichen(_bericht(FRITZ, PC), jetzt=T0) == [] assert daten == [] and netzgeraete.pruefe_netzgeraete() == [] handy = ("192.168.178.23", "6e:81:03:00:00:02", None) neu = netzgeraete.abgleichen(_bericht(FRITZ, PC, handy), jetzt=T0 + 1800) assert [g["mac"] for g in neu] == ["6e:81:03:00:00:02"] assert daten == [("Ein neues Gerät ist im Heimnetz aufgetaucht:\n• ohne Namen (192.168.178.23 · MAC " "6e:81:03:00:00:02 · zufällige MAC, oft ein Handy oder Tablet)\nKennst du es, drück im " "Homelab-Cockpit „Ist bekannt“.")] (befund,) = netzgeraete.pruefe_netzgeraete() assert (befund.id, befund.stufe, befund.titel) == ("netz:6e:81:03:00:00:02", "gelb", "Neues Gerät im Netz: 192.168.178.23") assert befund.aktionen == [{"id": "bekannt", "label": "Ist bekannt", "mac": "6e:81:03:00:00:02"}] and befund.sofort # Beim nächsten Bericht kein zweites Mal. assert netzgeraete.abgleichen(_bericht(FRITZ, PC, handy), jetzt=T0 + 3600) == [] and len(daten) == 1 assert netzgeraete.bestaetigen("6E:81:03:00:00:02")["ok"] assert netzgeraete.pruefe_netzgeraete() == [] assert not netzgeraete.bestaetigen("00:00:00:00:00:00")["ok"] def test_gaeste_und_neue_mac_mit_bekanntem_namen_sind_nicht_neu(daten): netzgeraete.abgleichen(_bericht(FRITZ, PC), jetzt=T0) gast = ("192.168.178.160", "bc:24:11:00:00:09", None) pc_neue_mac = ("192.168.178.22", "36:5a:60:00:00:99", "TobisNicerPC.fritz.box") assert netzgeraete.abgleichen(_bericht(FRITZ, gast, pc_neue_mac, gaeste=("192.168.178.160",)), jetzt=T0 + 60) == [] lage = netzgeraete.lage() pc = next(g for g in lage["geraete"] if g["mac"] == "36:5a:60:00:00:99") assert pc["wie"] == "34:5a:60:00:00:05" and pc["name"] == "TobisNicerPC" and pc["bestaetigt"] def test_ohne_netz_im_bericht_passiert_nichts(daten): assert netzgeraete.abgleichen({"host": {}}, jetzt=T0) == [] assert not (Path(einstellungen_mod.einstellungen().daten_dir) / "netz-bekannt.json").exists() def test_schnittstelle_und_knopf_des_hinweises(daten, monkeypatch): from routers import homelab from services import waechter netzgeraete.abgleichen(_bericht(FRITZ), jetzt=T0) netzgeraete.abgleichen(_bericht(FRITZ, PC), jetzt=T0 + 60) app = FastAPI() app.include_router(homelab.router) lage = TestClient(app).get("/api/homelab/netz").json() assert lage["netz"] == "192.168.178.0/24" and [g["bestaetigt"] for g in lage["geraete"]] == [False, True] (befund,) = netzgeraete.pruefe_netzgeraete() monkeypatch.setattr(waechter, "lese_stand", lambda: {"hinweise": [{"id": befund.id, "aktionen": befund.aktionen}]}) ergebnis = waechter.fuehre_aktion_aus(befund.id, "bekannt") assert ergebnis["ok"] and ergebnis["text"].startswith("Gemerkt: TobisNicerPC ist bekannt.")