"""Het invulmenu: stapindeling, inloggegevens en zelf bewerken. Het formulier was één lange lijst. Dat werkt slecht aan beide uiteinden: de mediane app heeft vijf velden in één groep, arr-stack heeft er tweeënvijftig in eenentwintig groepen. Vandaar vaste stappen in plaats van één stap per groep. """ import json import stat import sys import time from pathlib import Path import pytest sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up")) from conftest import login APPS = Path(__file__).resolve().parent.parent / "apps" @pytest.fixture def bp(env): from core import boilerplates return boilerplates def _veld(bp, **kw): """Eén los veld door fields() halen, om de indeling te toetsen.""" from core import git d = git.cache_dir("server-up") / "apps" / "los" (d / "files").mkdir(parents=True, exist_ok=True) item = {"name": "x", "type": "str", "title": "X"} item.update(kw) (d / "template.json").write_text(json.dumps({ "kind": "compose", "metadata": {"name": "los"}, "variables": [{"title": "Algemeen", "items": [item]}]}), encoding="utf-8") (d / "files" / "compose.yaml").write_text("services:\n a:\n image: nginx\n", encoding="utf-8") return bp.fields(d)[0] # ── Indeling in stappen ────────────────────────────────────────────────────── @pytest.mark.parametrize("kw,stap", [ ({"name": "service_name"}, "basis"), ({"name": "port", "type": "int"}, "basis"), ({"name": "web_port", "type": "int"}, "basis"), ({"name": "appdata_dir"}, "basis"), ({"name": "data_root"}, "basis"), ({"name": "timezone"}, "basis"), ({"name": "db_password"}, "geheimen"), ({"name": "jwt_secret"}, "geheimen"), ({"name": "admin_user"}, "geheimen"), # hoort bij het wachtwoord ({"name": "admin_email"}, "geheimen"), ({"name": "retention"}, "instellingen"), ({"name": "allow_signup", "type": "bool"}, "instellingen"), ({"name": "broker", "connect": {"app": "mosquitto"}}, "verbinden"), ]) def test_velden_komen_in_de_juiste_stap(bp, kw, stap): assert _veld(bp, **kw)["step"] == stap def test_sjabloon_mag_de_stap_overrulen(bp): """Een app-maker moet een veld ergens anders kunnen neerzetten.""" assert _veld(bp, name="retention", step="basis")["step"] == "basis" def test_onbekende_stap_valt_terug(bp): assert _veld(bp, name="retention", step="verzonnen")["step"] == "instellingen" @pytest.mark.parametrize("app,verwacht", [ ("grafana", {"basis", "geheimen"}), ("vaultwarden", {"basis", "instellingen", "geheimen"}), ]) def test_echte_apps_krijgen_zinnige_stappen(bp, app, verwacht): stappen = {f["step"] for f in bp.fields(APPS / app)} assert stappen == verwacht def test_de_vangnetstap_slokt_niet_alles_op(bp): """Een app waarvan alle velden in 'instellingen' belanden, betekent dat de indeling niets heeft herkend. Alles in 'basis' is juist prima: de meeste apps vragen alleen om een naam, een poort en een map.""" mislukt = [] for d in sorted(APPS.iterdir()): if not (d / "template.json").is_file(): continue velden = bp.fields(d) if len(velden) > 3 and {f["step"] for f in velden} == {"instellingen"}: mislukt.append(d.name) assert not mislukt, mislukt def test_eenvoudige_apps_krijgen_maar_een_paar_stappen(bp): """Voor de mediane app hoort het invulmenu kort te zijn.""" velden = bp.fields(APPS / "freshrss") assert {f["step"] for f in velden} == {"basis"} # ── Inloggegevens ──────────────────────────────────────────────────────────── @pytest.mark.parametrize("naam,soort", [ ("admin_password", "password"), ("web_password", "password"), ("admin_user", "username"), ("admin_email", "username"), ("superuser_name", "username"), ("jwt_secret", ""), # een sleutel is geen inloggegeven ("api_key", ""), ("port", ""), ]) def test_inloggegevens_worden_herkend(bp, naam, soort): assert _veld(bp, name=naam)["credential"] == soort def test_sjabloon_mag_credential_overrulen(bp): assert _veld(bp, name="iets", credential="username")["credential"] == "username" def _bron(env, naam="inlogapp"): from core import git d = git.cache_dir("server-up") / "apps" / naam (d / "files").mkdir(parents=True, exist_ok=True) (d / "template.json").write_text(json.dumps({ "kind": "compose", "metadata": {"name": naam}, "variables": [{"title": "Algemeen", "items": [ {"name": "service_name", "type": "str", "title": "Naam", "default": naam, "required": True}, {"name": "port", "type": "int", "title": "Poort", "default": 8123}, {"name": "admin_user", "type": "str", "title": "Gebruikersnaam", "default": "admin"}, {"name": "admin_password", "type": "str", "title": "Wachtwoord", "default": "", "required": True}, ]}]}), encoding="utf-8") (d / "files" / "compose.yaml").write_text( "services:\n << service_name >>:\n image: nginx\n" " ports:\n - \"<< port >>:80\"\n" " environment:\n" " - ADMIN_USER=<< admin_user >>\n" " - ADMIN_PASSWORD=<< admin_password >>\n", encoding="utf-8") return d def _wacht(client, jid, tijdslimiet=10): 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 _installeer(client, env, csrf, inst="ia", **waarden): _bron(env) v = {"service_name": inst, "admin_user": "ramon", "admin_password": "MijnWachtwoord1"} v.update(waarden) r = client.post("/api/store/install", json={"stack": "inlogapp", "repo_id": "server-up", "instance": inst, "values": v}, headers={"X-CSRF-Token": csrf}) assert r.status_code == 200, r.get_json() _wacht(client, r.get_json()["job_id"]) return r def test_inloggegevens_tonen_naam_en_wachtwoord(client, env): csrf = login(client) _installeer(client, env, csrf) d = client.get("/api/stacks/ia/credentials").get_json() assert d["ok"] is True soorten = {i["kind"]: i["value"] for i in d["items"]} assert soorten["username"] == "ramon" assert soorten["password"] == "MijnWachtwoord1" assert d["self_signup"] is False def test_wachtwoord_is_als_geheim_gemarkeerd(client, env): """De interface verbergt het achter een oog-knop.""" csrf = login(client) _installeer(client, env, csrf, inst="ib") d = client.get("/api/stacks/ib/credentials").get_json() wachtwoord = next(i for i in d["items"] if i["kind"] == "password") naam = next(i for i in d["items"] if i["kind"] == "username") assert wachtwoord["secret"] is True assert naam["secret"] is False def test_app_zonder_inlogvelden_zegt_dat(client, env): """Vaultwarden en Immich laten je bij het eerste bezoek zelf een account aanmaken; dan valt hier niets te tonen.""" from core import git csrf = login(client) d = git.cache_dir("server-up") / "apps" / "geeninlog" (d / "files").mkdir(parents=True, exist_ok=True) (d / "template.json").write_text(json.dumps({ "kind": "compose", "metadata": {"name": "geeninlog"}, "variables": [{"title": "Algemeen", "items": [ {"name": "service_name", "type": "str", "title": "Naam", "default": "geeninlog", "required": True}]}]}), encoding="utf-8") (d / "files" / "compose.yaml").write_text( "services:\n << service_name >>:\n image: nginx\n", encoding="utf-8") r = client.post("/api/store/install", json={"stack": "geeninlog", "repo_id": "server-up", "instance": "gi", "values": {}}, headers={"X-CSRF-Token": csrf}) _wacht(client, r.get_json()["job_id"]) res = client.get("/api/stacks/gi/credentials").get_json() assert res["items"] == [] assert res["self_signup"] is True def test_inloggegevens_alleen_voor_beheerders(client, env): """Ze staan gelijk aan toegang tot de app zelf.""" csrf = login(client) _installeer(client, env, csrf, inst="ic") client.post("/api/auth/users", json={"username": "op", "password": "eenlangwachtwoord", "role": "operator"}, headers={"X-CSRF-Token": csrf}) client.post("/api/auth/login", json={"username": "op", "password": "eenlangwachtwoord"}) r = client.get("/api/stacks/ic/credentials") assert r.status_code == 403 def test_tonen_komt_in_het_auditlog(client, env): csrf = login(client) _installeer(client, env, csrf, inst="id") from core import audit audit.init() client.get("/api/stacks/id/credentials") assert any(r["action"] == "credentials_view" for r in audit.query()) def test_onbekende_stack(client, env): login(client) assert client.get("/api/stacks/bestaatniet/credentials").status_code == 404 # ── Zelf bewerken vóór installeren ─────────────────────────────────────────── def test_eigen_compose_wordt_gebruikt(client, env): csrf = login(client) _bron(env) eigen = ("services:\n handmatig:\n image: nginx:1.27\n" " container_name: zelf-verzonnen\n") r = client.post("/api/store/install", json={"stack": "inlogapp", "repo_id": "server-up", "instance": "ie", "values": {"admin_password": "MijnWachtwoord1"}, "compose_override": eigen}, headers={"X-CSRF-Token": csrf}) assert r.status_code == 200, r.get_json() _wacht(client, r.get_json()["job_id"]) assert "zelf-verzonnen" in (env["lib"] / "ie" / "docker-compose.yml").read_text() def test_eigen_env_wordt_gebruikt(client, env): csrf = login(client) _bron(env) r = client.post("/api/store/install", json={"stack": "inlogapp", "repo_id": "server-up", "instance": "if", "values": {"admin_password": "MijnWachtwoord1"}, "env_override": "HANDMATIG=ja\n"}, headers={"X-CSRF-Token": csrf}) _wacht(client, r.get_json()["job_id"]) assert "HANDMATIG=ja" in (env["lib"] / "if" / ".env").read_text() def test_kapotte_yaml_wordt_geweigerd_voordat_er_iets_gemaakt_is(client, env): """Een typefout hoort hier te stranden, niet halverwege een installatie.""" csrf = login(client) _bron(env) r = client.post("/api/store/install", json={"stack": "inlogapp", "repo_id": "server-up", "instance": "ig", "values": {"admin_password": "MijnWachtwoord1"}, "compose_override": "services: [kapot: ja: nee"}, headers={"X-CSRF-Token": csrf}) assert r.status_code == 400 assert "YAML" in r.get_json()["msg"] assert not (env["lib"] / "ig").exists() # ── De .env-editor ─────────────────────────────────────────────────────────── def test_env_editor_weigert_een_regel_zonder_isgelijkteken(client, env): """Compose negeert zo'n regel stilzwijgend; dan mist er ineens een wachtwoord zonder dat je het merkt.""" csrf = login(client) _installeer(client, env, csrf, inst="ih") r = client.put("/api/stacks/ih/env", json={"content": "GOED=1\ndit is onzin\n"}, headers={"X-CSRF-Token": csrf}) assert r.status_code == 400 assert "Regel 2" in r.get_json()["msg"] def test_env_editor_accepteert_commentaar_en_lege_regels(client, env): csrf = login(client) _installeer(client, env, csrf, inst="ii") r = client.put("/api/stacks/ii/env", json={"content": "# uitleg\n\nGOED=1\n"}, headers={"X-CSRF-Token": csrf}) assert r.status_code == 200, r.get_json() assert stat.S_IMODE((env["lib"] / "ii" / ".env").stat().st_mode) == 0o600 def test_env_bewerken_komt_in_het_auditlog(client, env): csrf = login(client) _installeer(client, env, csrf, inst="ij") from core import audit audit.init() client.put("/api/stacks/ij/env", json={"content": "X=1\n"}, headers={"X-CSRF-Token": csrf}) assert any(r["action"] == "env_edit" for r in audit.query()) # ── Voorvertoning met eigen waarden ────────────────────────────────────────── def test_voorvertoning_gebruikt_de_ingevulde_waarden(client, env): """Anders toont de laatste stap iets anders dan er straks komt te staan.""" csrf = login(client) _bron(env) r = client.post("/api/store/preview", json={"stack": "inlogapp", "repo_id": "server-up", "values": {"service_name": "eigennaam", "port": 9999, "admin_password": "MijnWachtwoord1"}}, headers={"X-CSRF-Token": csrf}) d = r.get_json() assert "eigennaam" in d["compose_preview"] assert "9999" in d["compose_preview"] # Het geheim hoort al in de .env-voorvertoning te staan, niet in compose. assert "MijnWachtwoord1" not in d["compose_preview"] assert "MijnWachtwoord1" in d["env_preview"]