Files
mission-control-v2/backend/tests/test_kennzahlen.py
T
HitonabiandClaude Opus 5.5 f2bffc7203 Welle 1 · Nachschliff: Proxmox-Kachel zählt die Gäste wie die Homelab-Karte, der eigene Container heißt lesbar
„8 von 8 Gästen an“ neben „7 Gäste“: Die Kachel zählte den eigenen Container (Etikett mc2) mit, die Karte nicht.
In „Sicherungen je Gerät“ heißt er „Homelab Orchestrator (dieser Container)“ statt homelab-orchestrator.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-25 17:16:22 +02:00

151 lines
7.8 KiB
Python

"""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