"""Live-Zahlen je Gerät auf den Kacheln (Ausbauplan Welle 1, 25.09.2026) und der AdGuard-Zugang dafür.""" import json import os from pathlib import Path import httpx import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from kern import einstellungen as einstellungen_mod from services.homelab import adguard, arcane, kanal, kennzahlen, zertifikate BERICHT = json.loads((Path(__file__).parent / "fixtures" / "pve-bericht.json").read_text(encoding="utf-8")) PASSWORT = "geheim-Passwort-123" @pytest.fixture def daten(tmp_path, monkeypatch): monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) einstellungen_mod.einstellungen.cache_clear() adguard._cache.clear() arcane.cache_leeren() yield tmp_path einstellungen_mod.einstellungen.cache_clear() adguard._cache.clear() arcane.cache_leeren() def _bericht() -> dict: b = json.loads(json.dumps(BERICHT)) b["host"]["sicherungsziele"] = {"pbs-qnap": {"neueste": {"102": 1790296227, "104": 1790296257, "106": 1790296281}}} return b def test_zahlen_je_geraet(monkeypatch): npm = {"quelle": "npm", "gast": 101, "stufe": "ok"} monkeypatch.setattr(zertifikate, "lage", lambda jetzt=None: {"zertifikate": [npm] * 10}) monkeypatch.setattr(arcane, "container_zahlen", lambda url: {"laufend": 11, "gesamt": 12}) monkeypatch.setattr(adguard, "geblockt", lambda: {"anfragen": 84_211, "geblockt": 10_523, "anteil": 12.5}) b = _bericht() b["gaeste"].append({"vmid": 107, "art": "lxc", "status": "running", "etiketten": ["mc2"]}) # der eigene zählt nicht zahlen = kennzahlen.fuer_bericht(b) laufend = sum(1 for g in BERICHT["gaeste"] if g["status"] == "running") assert zahlen["pve"] == {"text": f"{laufend} von {len(BERICHT['gaeste'])} Gästen an"} assert zahlen["ct-105"] == {"text": "letzte Sicherung", "zeit": 1790296281} assert zahlen["ct-101"] == {"text": "10 Zertifikate gültig"} assert zahlen["vm-106"] == {"text": "11 von 12 Containern an"} assert zahlen["ct-100"] == {"text": "12 % geblockt"} assert "ct-104" not in zahlen # Gitea: keine Zahl monkeypatch.setattr(zertifikate, "lage", lambda jetzt=None: {"zertifikate": [npm, {**npm, "stufe": "gelb"}]}) monkeypatch.setattr(adguard, "geblockt", lambda: {"anteil": 4.25}) monkeypatch.setattr(arcane, "container_zahlen", lambda url: (_ for _ in ()).throw(RuntimeError("weg"))) zahlen = kennzahlen.fuer_bericht(_bericht()) assert zahlen["ct-101"] == {"text": "1 von 2 Zertifikaten mit Problem", "stufe": "gelb"} assert zahlen["ct-100"] == {"text": "4,2 % geblockt"} and "vm-106" not in zahlen # ein Fehler kostet nur die Zahl def test_ziele_tragen_die_kennzahl(daten, monkeypatch): from services.homelab import inventar monkeypatch.setattr(inventar, "erreichbar", lambda url: True) monkeypatch.setattr(inventar, "neueste_version", lambda repo: None) monkeypatch.setattr(kennzahlen, "fuer_bericht", lambda b: {"pve": {"text": "8 von 8 Gästen an"}}) kanal.bericht_speichern(_bericht()) ziele = {z["id"]: z for z in inventar.ziele()["ziele"]} assert ziele["pve"]["kennzahl"] == {"text": "8 von 8 Gästen an"} and ziele["ct-104"]["kennzahl"] is None monkeypatch.setattr(kennzahlen, "fuer_bericht", lambda b: 1 / 0) assert all(z["kennzahl"] is None for z in inventar.ziele()["ziele"]) # die Liste hängt nie daran def test_arcane_zaehlt_ueber_alle_umgebungen(daten, monkeypatch): monkeypatch.setenv("MC_ARCANE_KEY", "schluessel-123") monkeypatch.setattr(arcane, "umgebungen", lambda url, mit_schluessel=None: [{"id": 0}, {"id": 1}]) antworten = {0: {"data": {"runningContainers": 9, "totalContainers": 10}}, 1: {"data": {"runningContainers": 2, "totalContainers": 2}}} monkeypatch.setattr(arcane, "_get", lambda url, pfad, s=None: antworten[int(pfad.split("/")[2])]) assert arcane.container_zahlen("http://arcane:3552") == {"laufend": 11, "gesamt": 12} monkeypatch.delenv("MC_ARCANE_KEY") arcane.cache_leeren() assert arcane.container_zahlen("http://arcane:3552") is None # ohne Schlüssel: nichts class _Antwort: def __init__(self, status: int, daten: object): self.status_code, self._daten = status, daten def raise_for_status(self): if self.status_code >= 400: raise httpx.HTTPStatusError("x", request=httpx.Request("GET", "http://x"), response=self) def json(self): return self._daten def test_adguard_statistik(monkeypatch): gesehen = [] monkeypatch.setattr(adguard.httpx, "get", lambda url, auth, timeout: gesehen.append((url, auth)) or _Antwort( 200, {"num_dns_queries": 1000, "num_blocked_filtering": 125})) assert adguard.statistik("http://192.168.178.100:8080", "orchestrator", PASSWORT) == { "ok": True, "anfragen": 1000, "geblockt": 125, "anteil": 12.5} assert gesehen == [("http://192.168.178.100:8080/control/stats", ("orchestrator", PASSWORT))] monkeypatch.setattr(adguard.httpx, "get", lambda url, auth, timeout: _Antwort(401, {})) assert adguard.statistik("http://x", "a", "b") == {"ok": False, "status": 401, "art": "abgelehnt"} def weg(url, auth, timeout): raise httpx.ConnectError("weg") monkeypatch.setattr(adguard.httpx, "get", weg) assert adguard.statistik("http://x", "a", "b")["art"] == "netz" monkeypatch.setattr(adguard.httpx, "get", lambda url, auth, timeout: _Antwort(200, {"anders": 1})) assert adguard.statistik("http://x", "a", "b")["art"] == "antwort" def test_adguard_zugang_erst_pruefen_dann_speichern(daten, monkeypatch): from routers import homelab kanal.bericht_speichern(_bericht()) app = FastAPI() app.include_router(homelab.router) client = TestClient(app) antworten = {"ok": False, "status": 401, "art": "abgelehnt"} monkeypatch.setattr(adguard, "statistik", lambda url, b, p: antworten) abgelehnt = client.post("/api/homelab/einstellungen/adguard-zugang", json={"benutzer": "orchestrator", "passwort": PASSWORT}).json() assert abgelehnt == {"ok": False, "detail": "AdGuard lehnt Benutzer oder Passwort ab (HTTP 401). Nichts gespeichert."} assert adguard.zugang() is None antworten = {"ok": True, "anfragen": 1000, "geblockt": 125, "anteil": 12.5} monkeypatch.setattr(adguard, "statistik", lambda url, b, p: antworten) gut = client.post("/api/homelab/einstellungen/adguard-zugang", json={"benutzer": " orchestrator ", "passwort": PASSWORT}).json() assert gut["ok"] is True and "davon 12,5 % geblockt" in gut["text"] and PASSWORT not in json.dumps(gut) assert adguard.zugang() == ("orchestrator", PASSWORT) if os.name != "nt": assert oct((daten / "adguard.json").stat().st_mode & 0o777) == "0o600" stand = client.get("/api/homelab/einstellungen").json()["adguard"] assert stand == {"url": "http://192.168.178.100:8080", "zugang": "hinterlegt", "benutzer": "orchestrator"} assert PASSWORT not in json.dumps(client.get("/api/homelab/einstellungen").json()) kaputt = client.post("/api/homelab/einstellungen/adguard-zugang", content=b"passwort=" + PASSWORT.encode()) assert kaputt.json()["ok"] is False and PASSWORT not in kaputt.text assert client.delete("/api/homelab/einstellungen/adguard-zugang").json()["text"] == "Der AdGuard-Zugang ist gelöscht." assert adguard.zugang() is None def test_adguard_kachel_fragt_hoechstens_alle_zwei_minuten(daten, monkeypatch): kanal.bericht_speichern(_bericht()) adguard.zugang_speichern("orchestrator", PASSWORT) fragen = [] monkeypatch.setattr(adguard, "statistik", lambda url, b, p: fragen.append(url) or {"ok": True, "anteil": 7.0}) assert adguard.geblockt() == {"ok": True, "anteil": 7.0} assert adguard.geblockt() == {"ok": True, "anteil": 7.0} and len(fragen) == 1