"""Wartungsfenster und Update-Regel je Gerät (Ausbauplan Welle 3, Punkt 17, 25.09.2026). User-Entscheid: So 03:00–04:15, zu Beginn für alle Geräte „Sicherheitsupdates automatisch“, alles andere nur melden; der Host startet nie von selbst.""" from datetime import datetime import pytest from fastapi import FastAPI from fastapi.testclient import TestClient from kern import einstellungen as einstellungen_mod from kern.zeit import LOCAL_TZ from services.homelab import wartung SONNTAG_0310 = datetime(2026, 9, 27, 3, 10, tzinfo=LOCAL_TZ).timestamp() SONNTAG_0420 = datetime(2026, 9, 27, 4, 20, tzinfo=LOCAL_TZ).timestamp() SAMSTAG_0310 = datetime(2026, 9, 26, 3, 10, tzinfo=LOCAL_TZ).timestamp() @pytest.fixture def daten(tmp_path, monkeypatch): monkeypatch.setenv("MC_DATEN_DIR", str(tmp_path)) einstellungen_mod.einstellungen.cache_clear() yield tmp_path einstellungen_mod.einstellungen.cache_clear() def _b(bid: str, zustand: str = "neu", sicherheit: int | None = None, aktion: bool = True) -> dict: return {"id": bid, "zustand": zustand, "sicherheit": sicherheit, "aktion": {"pfad": "/x"} if aktion else None} ZIELE = [ {"id": "pve", "name": "Proxmox-Host", "bausteine": [_b("pakete", sicherheit=2), _b("neustart")]}, {"id": "ct-100", "name": "AdGuard Home", "bausteine": [_b("os", sicherheit=0)]}, {"id": "ct-104", "name": "Gitea", "bausteine": [_b("app"), _b("os", sicherheit=3)]}, {"id": "vm-106", "name": "Arcane (Docker)", "bausteine": [_b("docker")]}, ] def test_fenster(daten): assert wartung.im_fenster(SONNTAG_0310) and not wartung.im_fenster(SONNTAG_0420) assert not wartung.im_fenster(SAMSTAG_0310) assert wartung.fenster_text() == "So 03:00–04:15" assert wartung.naechstes(SAMSTAG_0310) == datetime(2026, 9, 27, 3, 0, tzinfo=LOCAL_TZ).timestamp() assert wartung.naechstes(SONNTAG_0310) == datetime(2026, 9, 27, 3, 0, tzinfo=LOCAL_TZ).timestamp() # läuft gerade assert wartung.naechstes(SONNTAG_0420) == datetime(2026, 10, 4, 3, 0, tzinfo=LOCAL_TZ).timestamp() def test_plan_nach_regel(daten): # Standard „sicherheit“: nur Sicherheitsupdates, der Host ohne Neustart. assert [(s["ziel"], s["baustein"]) for s in wartung.plan(ZIELE)] == [("pve", "sicherheit"), ("ct-104", "sicherheit")] wartung.setzen({"regeln": {"ct-104": "alles", "pve": "melden", "vm-106": "alles"}}) assert [(s["ziel"], s["baustein"]) for s in wartung.plan(ZIELE)] == [ ("ct-104", "app"), ("ct-104", "os"), ("vm-106", "docker")] wartung.setzen({"regeln": {"ct-104": None}, "standard": "melden"}) assert [(s["ziel"], s["baustein"]) for s in wartung.plan(ZIELE)] == [("vm-106", "docker")] def test_einstellungen_pruefen(daten): assert wartung.setzen({"fenster": {"tag": 6, "von": "05:00", "bis": "04:00"}})["ok"] is False assert wartung.setzen({"fenster": {"tag": 9, "von": "03:00", "bis": "04:00"}})["ok"] is False assert wartung.setzen({"regeln": {"ct-104": "immer"}})["ok"] is False assert wartung.setzen({"regeln": {"rm -rf": "alles"}})["ok"] is False gut = wartung.setzen({"fenster": {"tag": 5, "von": "02:30", "bis": "04:00"}}) assert gut == {"ok": True, "text": "Gespeichert. Wartungsfenster Sa 02:30–04:00."} def test_takt_einmal_je_sonntag_und_nie_neben_einem_update(daten, monkeypatch): from services.homelab import inventar, sammellauf, updates monkeypatch.setattr(inventar, "ziele", lambda: {"ziele": ZIELE}) monkeypatch.setattr(sammellauf, "laeuft", lambda: False) laufend = [{"ct-100"}] monkeypatch.setattr(updates, "laufende_ziele", lambda: laufend[0]) gestartet: list[dict] = [] monkeypatch.setattr(wartung.threading, "Thread", lambda target, args, name, daemon: type( "T", (), {"start": lambda self: gestartet.append(args[0])})()) assert wartung.takt(SAMSTAG_0310) is False assert wartung.takt(SONNTAG_0310) is False and gestartet == [] # ein Update läuft: nächste Minute laufend[0] = set() assert wartung.takt(SONNTAG_0310) is True and len(gestartet) == 1 assert wartung.takt(SONNTAG_0310 + 60) is False # heute schon gelaufen assert wartung.lage(SONNTAG_0310)["stand"]["schritte"][0]["baustein"] == "sicherheit" def test_abarbeiten_und_zusammenfassung(daten, monkeypatch): from services.homelab import updates monkeypatch.setattr(wartung, "im_fenster", lambda jetzt=None, fenster=None: True) gestartet: list[tuple] = [] ergebnisse = {"pve": "eingespielt", "ct-104": "zurueckgerollt"} def starten(zid, baustein, sammellauf=None): gestartet.append((zid, baustein, sammellauf["anlass"])) return {"ok": True, "lauf": zid} monkeypatch.setattr(updates, "starten", starten) monkeypatch.setattr(updates, "lauf_lesen", lambda lid: {"status": "fertig", "ergebnis": ergebnisse[lid], "text": "…"}) meldungen: list[str] = [] monkeypatch.setattr(wartung, "_melden", meldungen.append) stand = {"datum": "2026-09-27", "status": "laeuft", "start": SONNTAG_0310, "schritte": wartung.plan(ZIELE)} wartung._abarbeiten(stand) assert gestartet == [("pve", "sicherheit", "Wartungsfenster"), ("ct-104", "sicherheit", "Wartungsfenster")] assert wartung.stand_lesen()["status"] == "fertig" assert meldungen == [("Wartung So 03:00–04:15: Eingespielt: Proxmox-Host (Sicherheitsupdates). Zurückgerollt (läuft " "wie vorher): Gitea (Sicherheitsupdates).")] def test_version_ueberspringen_und_schnittstelle(daten): from routers import homelab app = FastAPI() app.include_router(homelab.router) client = TestClient(app) assert client.post("/api/homelab/ziele/ct-104/ueberspringen", json={"version": "1.27.4"}).json()["ok"] is True assert wartung.uebersprungen("ct-104", "app") == "1.27.4" lage = client.get("/api/homelab/wartung").json() assert (lage["fenster_text"], lage["standard"], lage["uebersprungen"]) == ( "So 03:00–04:15", "sicherheit", {"ct-104:app": "1.27.4"}) assert client.delete("/api/homelab/ziele/ct-104/ueberspringen").json()["text"] == "Die Version wird wieder angeboten." assert wartung.uebersprungen("ct-104", "app") is None assert client.post("/api/homelab/wartung", json={"regeln": {"ct-104": "alles"}}).json()["ok"] is True assert client.post("/api/homelab/wartung", json={"standard": "nie"}).status_code == 409