server-up/tests/test_reconfigure.py
Ramon bc55252267
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m34s
v0.7.70-beta - invulmenu bij installeren, geheimen naar .env
Invulmenu:
- Vaste stappen (Basis, Verbinden, Instellingen, Toegang, Controleren) in
  plaats van één lange lijst. Eén stap per groep werkt niet: de mediane app
  heeft één groep, arr-stack eenentwintig. Lege stappen worden overgeslagen.
- Volgende controleert de verplichte velden van die stap; die melding kwam
  eerder pas bij het installeren.
- Geavanceerde opties achter één schakelaar die op elke stap zichtbaar is,
  plus 'Alles op één pagina' voor het oude gedrag. Beide onthouden.

Geheimen naar .env:
- compose_transform.geheimen_naar_env() vervangt geheime waarden door
  ${NAAM} en schrijft ze naar .env met rechten 0600. Werkt ook midden in
  een database-URL. Poorten en paden blijven leesbaar in compose.
- .serverup.json bevat geen geheimen meer en krijgt ook 0600; daar stonden
  ze wereldleesbaar in. Het herconfiguratieformulier leest ze terug uit
  .env via een bewaarde veld-naar-sleutel-koppeling.
- docs/beveiliging.md legt uit wat dit niet oplost: docker inspect toont de
  waarde nog steeds.

Bewerken:
- Laatste stap toont het gerenderde resultaat met jouw waarden en is
  bewerkbaar; compose_override/env_override gaan mee bij het installeren.
  Ongeldige YAML wordt geweigerd voordat er een map bestaat.
- De bestaande editor heeft tabbladen (compose/.env) en biedt herstarten na
  opslaan. De .env-PUT weigert een regel zonder '=' en logt in audit.

Inloggegevens:
- /api/stacks/<n>/credentials plus paneel op de stackkaart: adres,
  gebruikersnaam, wachtwoord achter een oog-knop, kopieerknop. Alleen voor
  beheerders, elk bekijken komt in het auditlog.
- Nieuw 'credential'-kenmerk in template.json met naamherkenning als
  terugval, zodat de bestaande 96 sjablonen meteen werken.

Herstel van een regressie uit v0.7.60: acht inlogwachtwoorden hadden hun
genereerknop verloren toen die aan 'generate' werd gehangen. Terug, behalve
waar een verzonnen waarde fout is (WireGuard-sleutel, externe tokens).

De menulogica wordt getest door de echte component uit index.html in Node uit
te voeren; slaat zichzelf over waar node ontbreekt. 2295 tests groen.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01C7oLCRYzY5ixJ5Sv8Y8EFb
2026-07-28 21:31:51 +02:00

201 lines
7.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 _wacht_op_job(client, jid, tijdslimiet=10):
"""Installeren gebeurt in een achtergrondthread; wachten tot hij klaar is."""
einde = time.time() + tijdslimiet
while time.time() < einde:
d = client.get(f"/api/jobs/{jid}").get_json() or {}
if d.get("status") in ("done", "error"):
return d
time.sleep(0.05)
return {}
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.
Was een controle op een letterlijke regel broncode; die brak zodra de regel
verplaatste terwijl het gedrag klopte.
"""
import json as _json
from core import git
csrf = login(client)
wortel = git.cache_dir("server-up") / "apps" / "keuzeapp"
(wortel / "files").mkdir(parents=True, exist_ok=True)
(wortel / "template.json").write_text(_json.dumps({
"kind": "compose", "metadata": {"name": "keuzeapp"},
"variables": [{"title": "Algemeen", "items": [
{"name": "service_name", "type": "str", "title": "Naam",
"default": "keuzeapp", "required": True},
{"name": "port", "type": "int", "title": "Poort", "default": 8099},
]}]}), encoding="utf-8")
(wortel / "files" / "compose.yaml").write_text(
'services:\n << service_name >>:\n image: nginx\n'
' ports:\n - "<< port >>:80"\n', encoding="utf-8")
r = client.post("/api/store/install",
json={"stack": "keuzeapp", "repo_id": "server-up",
"instance": "kz", "values": {"port": 9123}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
_wacht_op_job(client, r.get_json()["job_id"])
meta = _json.loads((env["lib"] / "kz" / ".serverup.json").read_text())
assert meta["values"]["port"] == 9123
assert meta["source"] == "keuzeapp"
# ── 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