server-up/tests/test_reconfigure.py
Ramon 2ba15418ab
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m13s
v0.7.20-beta - instellingen wijzigen, koppelen achteraf, defect uit v0.7.00
Defect (v0.7.00): add_shared_network() hing elke service aan het gedeelde
netwerk, ook services met network_mode. Docker compose weigert die combinatie
("declares mutually exclusive network_mode and networks"), waardoor Homebridge,
Scrypted, Beszel en de ARR-stack met Gluetun niet te installeren waren met de
standaardkeuze "Verbinden met andere apps". Die services worden nu overgeslagen;
blijft er niets over, dan ook geen netwerkblok. Test over alle 96 apps.

Instellingen wijzigen na installatie:
- De gemaakte keuzes worden bij installatie opgeslagen in .serverup.json
  (values + format); zonder die waarden viel het formulier niet te heropenen.
- GET /api/stacks/<naam>/config geeft de opgeslagen waarden plus het actuele
  veldschema; POST /reconfigure maakt een backup, rendert opnieuw, past netwerk
  en eigen IP opnieuw toe, valideert en herstart. Mislukt het valideren, dan
  wordt de backup teruggezet.
- Knop Instellingen op de stackkaart opent dezelfde modal in wijzigen-stand,
  met een waarschuwing dat handmatige compose-wijzigingen verloren gaan.
- Stacks van voor deze versie: knop uit met uitleg.

Bestaande stacks koppelen:
- POST /api/stacks/<naam>/connect zet een bestaande stack op het gedeelde
  netwerk, valideert en herstart; wordt de compose ongeldig, dan wordt de
  wijziging teruggedraaid.

Audit-log:
- audit.query()/count() accepteren filters op bron, actie, status, periode en
  vrije tekst; facets() levert de keuzelijsten. /api/audit ondersteunt die als
  queryparameters plus doorbladeren. Filterbalk in de UI.

Documentatie:
- README.md (bestond niet), docs/apps-maken.md met het volledige
  templateformaat, docs/README.md als index.
- Changelog-secties voor v0.5.44/45/46 aangevuld; release.yml haalt de notes
  daaruit, dus een tag daarop gaf een lege release. Test die afdwingt dat het
  huidige VERSION een sectie heeft.

941 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-27 15:43:49 +02:00

163 lines
6 KiB
Python

"""Instellingen wijzigen na installatie, koppelen, en het audit-filter."""
import json
import sys
import time
from pathlib import Path
import pytest
WORTEL = Path(__file__).resolve().parent.parent
sys.path.insert(0, str(WORTEL / "server-up"))
from conftest import login
def _stack(env, naam="web", meta=None, compose=None):
d = env["lib"] / naam
d.mkdir(parents=True, exist_ok=True)
(d / "docker-compose.yml").write_text(
compose or "services:\n web:\n image: nginx\n ports:\n - \"8080:80\"\n")
if meta is not None:
(d / ".serverup.json").write_text(json.dumps(meta), encoding="utf-8")
return d
# ── Herconfigureren ──────────────────────────────────────────────────────────
def test_config_zonder_opgeslagen_waarden(client, env):
"""Stacks van vóór deze versie hebben geen keuzes bewaard; dan moet de
interface uitleggen waarom de knop niet werkt in plaats van te falen."""
login(client)
_stack(env, "oud", {"source": "oud"})
d = client.get("/api/stacks/oud/config").get_json()
assert d["ok"] is False and d["editable"] is False
assert "compose bewerken" in d["msg"]
def test_config_van_onbekende_stack(client):
login(client)
assert client.get("/api/stacks/bestaatniet/config").status_code == 404
def test_stacklijst_meldt_of_wijzigen_kan(client, env):
login(client)
_stack(env, "nieuw", {"source": "x", "values": {"port": 80}})
_stack(env, "oud", {"source": "x"})
per_naam = {s["name"]: s for s in client.get("/api/stacks").get_json()}
assert per_naam["nieuw"]["editable"] is True
assert per_naam["oud"]["editable"] is False
def test_reconfigure_zonder_bronsjabloon(client, env):
csrf = login(client)
_stack(env, "web", {"source": "bestaatniet", "repo_id": "x", "values": {"a": 1}})
r = client.post("/api/stacks/web/reconfigure", json={"values": {"a": 2}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert "Bronsjabloon" in r.get_json()["msg"]
def test_reconfigure_van_onbekende_stack(client):
csrf = login(client)
r = client.post("/api/stacks/bestaatniet/reconfigure", json={"values": {}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 404
def test_installatie_bewaart_de_gemaakte_keuzes(client, env, monkeypatch):
"""Zonder opgeslagen waarden valt het formulier later niet te heropenen."""
import app as app_module
assert '"values": bp_values if is_bp else {}' in \
(WORTEL / "server-up" / "app.py").read_text(encoding="utf-8")
# ── Koppelen ─────────────────────────────────────────────────────────────────
def test_connect_van_onbekende_stack(client):
csrf = login(client)
r = client.post("/api/stacks/bestaatniet/connect", json={"connect": True},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 404
def test_connect_start_een_job(client, env):
csrf = login(client)
_stack(env, "web", {"source": "x"})
d = client.post("/api/stacks/web/connect", json={"connect": True},
headers={"X-CSRF-Token": csrf}).get_json()
assert d["ok"] and d["job_id"]
def test_viewer_mag_niet_herconfigureren(client, env):
csrf = login(client)
_stack(env, "web", {"source": "x", "values": {}})
client.post("/api/auth/users",
json={"username": "kijker", "password": "eenlangwachtwoord",
"role": "viewer"}, headers={"X-CSRF-Token": csrf})
kijker = client.post("/api/auth/login",
json={"username": "kijker", "password": "eenlangwachtwoord"}
).get_json()["csrf_token"]
assert client.post("/api/stacks/web/reconfigure", json={"values": {}},
headers={"X-CSRF-Token": kijker}).status_code == 403
# ── Audit-filter ─────────────────────────────────────────────────────────────
@pytest.fixture
def gevuld(env):
from core import audit
audit.init()
audit.log("auth", "login", "ok", ref="ramon", ip="10.0.0.1")
audit.log("auth", "login", "error", ref="indringer", ip="10.9.9.9")
audit.log("stacks", "start", "ok", ref="jellyfin")
audit.log("backups", "create", "ok", ref="jellyfin")
return audit
def test_filter_op_bron(gevuld):
assert len(gevuld.query(100, src="auth")) == 2
assert gevuld.count(src="auth") == 2
def test_filter_op_actie_en_status(gevuld):
r = gevuld.query(100, src="auth", action="login", status="error")
assert len(r) == 1 and r[0]["ref"] == "indringer"
def test_zoeken_in_referentie_en_ip(gevuld):
assert len(gevuld.query(100, zoek="jellyfin")) == 2
assert len(gevuld.query(100, zoek="10.9.9.9")) == 1
def test_filter_op_periode(gevuld):
assert gevuld.count(sinds=time.time() - 60) == 4
assert gevuld.count(sinds=time.time() + 60) == 0
def test_doorbladeren(gevuld):
eerste = gevuld.query(2, 0)
tweede = gevuld.query(2, 2)
assert len(eerste) == 2 and len(tweede) == 2
assert {r["id"] for r in eerste}.isdisjoint({r["id"] for r in tweede})
def test_facetten_voor_de_keuzelijsten(gevuld):
f = gevuld.facets()
assert set(f["src"]) == {"auth", "stacks", "backups"}
assert "login" in f["action"]
assert set(f["status"]) == {"ok", "error"}
def test_audit_endpoint_filtert(client, env, gevuld):
login(client)
d = client.get("/api/audit?src=auth&status=error").get_json()
assert d["total"] == 1
assert d["entries"][0]["ref"] == "indringer"
assert "auth" in d["facets"]["src"]
assert d["grand_total"] >= d["total"]
def test_audit_endpoint_limiet_wordt_begrensd(client, env, gevuld):
login(client)
d = client.get("/api/audit?limit=99999").get_json()
assert d["limit"] <= 500