All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m34s
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
201 lines
7.6 KiB
Python
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
|