server-up/tests/test_verplichte_velden.py
Ramon 952e7ddb7a
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m27s
v0.7.60-beta - app-instellingen nagelopen op fouten bij het installeren
Aanleiding: Homepage weigerde met "Host validation failed" omdat de
standaardwaarde van allowed_hosts poort 3001 noemde terwijl het poortveld op
3002 stond. Alle 96 sjablonen zijn daarop nagelopen.

Systemisch:
- De dobbelsteenknop, waar zeventien apps in hun uitleg naar verwijzen,
  verscheen nooit: hij hing af van een veld 'secret' dat fields() niet
  meestuurde. Wachtwoorden stonden daardoor leesbaar in het formulier.
- 27 velden die je toch nooit zelf typt (databasewachtwoorden, JWT- en
  versleutelingssleutels) worden nu voorgevuld met een willekeurige waarde.
  Inlogwachtwoorden krijgen alleen een knop, want die moet je noteren.
- De knop hangt nu aan 'generate' in plaats van aan 'secret': voor een
  WireGuard-privésleutel of een token uit een andere app is een verzonnen
  waarde juist fout.
- required werd nergens gecontroleerd. Je kon installeren met een leeg
  databasewachtwoord of een lege sleutel. Nu geweigerd bij installeren en bij
  herconfigureren, met vermelding van de lege velden. Velden achter een
  uitgeschakelde groepsschakelaar tellen niet mee.
- Wachtwoorden werden uitgeschreven in het installatielog; nu gemaskeerd.

Losse fouten:
- homepage: allowed_hosts stond op localhost:3001, nu * met uitleg.
- baserow, hedgedoc, ntfy: URL in de standaardwaarde wees naar een poort waar
  niets luistert.
- Vast wachtwoord in acht sjablonen weggehaald: ghost, immich, miniflux,
  paperless-ngx, unifi-network en vikunja kregen allemaal dezelfde
  database-wachtwoorden; grafana en gotify stonden op 'admin'.
- data_dir bij keycloak, metube, miniflux en teslamate stond in het formulier
  maar kwam nergens terecht (die apps gebruiken een Docker-volume).
- puid/pgid bij freshrss: het image kent ze niet.
- prometheus gaf de gevraagde tijdzone niet door.

Nieuw tests/test_app_templates.py: rendert elke app met zijn standaardwaarden
en controleert ongedefinieerde variabelen, velden zonder werking,
niet-gedeclareerde volumes, depends_on, dubbele containernamen,
poortconflicten, poortnummers in standaardwaarden en vaste wachtwoorden.
2236 tests groen.

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

160 lines
6.5 KiB
Python

"""Verplichte velden mogen niet leeg blijven, en geheimen horen gemaskeerd.
Het formulier zette een sterretje achter een verplicht veld en hield verder
niets tegen: je kon een app installeren met een leeg databasewachtwoord of een
lege JWT-sleutel. De container start dan niet, of draait met een leeg geheim.
"""
import json
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
from conftest import login
def _maak_bron(env, naam="testapp", items=None, groepen=None):
"""Zet een app-sjabloon neer waar _find_stack_src het verwacht."""
from core import git
wortel = git.cache_dir("server-up") / "apps" / naam
(wortel / "files").mkdir(parents=True, exist_ok=True)
variabelen = groepen or [{"title": "Algemeen", "items": items or []}]
(wortel / "template.json").write_text(json.dumps({
"kind": "compose",
"metadata": {"name": naam, "description": "test"},
"variables": variabelen,
}), encoding="utf-8")
(wortel / "files" / "compose.yaml").write_text(
"services:\n"
" << service_name >>:\n"
" image: nginx\n"
" environment:\n"
" - DB_PASSWORD=<< db_password >>\n", encoding="utf-8")
return wortel
VELDEN = [
{"name": "service_name", "type": "str", "title": "Naam",
"default": "testapp", "required": True},
{"name": "db_password", "type": "str", "title": "Databasewachtwoord",
"default": "", "required": True},
]
def test_leeg_verplicht_veld_wordt_geweigerd(client, env):
csrf = login(client)
_maak_bron(env, items=VELDEN)
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t1", "values": {"db_password": ""}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400, r.get_json()
d = r.get_json()
assert "Databasewachtwoord" in d["msg"]
assert "Databasewachtwoord" in d["missing"]
def test_ontbrekend_veld_wordt_geweigerd(client, env):
"""Niet meegestuurd is net zo fout als leeg meegestuurd."""
csrf = login(client)
_maak_bron(env, items=VELDEN)
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t2", "values": {}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert "Databasewachtwoord" in r.get_json()["missing"]
def test_alleen_spaties_telt_als_leeg(client, env):
csrf = login(client)
_maak_bron(env, items=VELDEN)
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t3", "values": {"db_password": " "}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
def test_ingevuld_veld_gaat_door(client, env):
csrf = login(client)
_maak_bron(env, items=VELDEN)
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t4", "values": {"db_password": "geheim123"}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
assert r.get_json().get("job_id")
def test_veld_met_een_default_hoeft_niet_meegestuurd(client, env):
"""service_name heeft een standaardwaarde; die telt als ingevuld."""
csrf = login(client)
_maak_bron(env, items=VELDEN)
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t5", "values": {"db_password": "x"}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
def test_veld_in_uitgeschakelde_groep_telt_niet_mee(client, env):
"""Staat de groepsschakelaar uit, dan komt het veld niet in compose terecht
en mag het dus leeg blijven."""
csrf = login(client)
_maak_bron(env, groepen=[
{"title": "Algemeen", "items": [VELDEN[0]]},
{"title": "Database", "toggle": "gebruik_db", "toggle_default": False,
"items": [{"name": "db_password", "type": "str", "title": "Wachtwoord",
"default": "", "required": True}]},
])
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t6",
"values": {"gebruik_db": False, "db_password": ""}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 200, r.get_json()
def test_veld_in_ingeschakelde_groep_telt_wel_mee(client, env):
csrf = login(client)
_maak_bron(env, groepen=[
{"title": "Algemeen", "items": [VELDEN[0]]},
{"title": "Database", "toggle": "gebruik_db", "toggle_default": True,
"items": [{"name": "db_password", "type": "str", "title": "Wachtwoord",
"default": "", "required": True}]},
])
r = client.post("/api/store/install",
json={"stack": "testapp", "repo_id": "server-up",
"instance": "t7",
"values": {"gebruik_db": True, "db_password": ""}},
headers={"X-CSRF-Token": csrf})
assert r.status_code == 400
assert "Wachtwoord" in r.get_json()["missing"]
# ── Geheimen herkennen ───────────────────────────────────────────────────────
@pytest.mark.parametrize("naam", [
"db_password", "admin_password", "jwt_secret", "secret_key", "api_key",
"app_key", "encryption_key", "admin_token", "hass_token",
])
def test_geheime_velden_worden_herkend(env, naam):
"""Zonder deze markering toonde de interface het wachtwoord als gewone
tekst én verscheen de dobbelsteenknop niet, terwijl de beschrijving van
zeventien apps ernaar verwijst."""
from core import boilerplates
wortel = _maak_bron(env, items=[
{"name": naam, "type": "str", "title": "X", "default": "", "required": True}])
veld = boilerplates.fields(wortel)[0]
assert veld["secret"] is True
@pytest.mark.parametrize("naam", ["service_name", "port", "data_dir", "timezone"])
def test_gewone_velden_zijn_geen_geheim(env, naam):
from core import boilerplates
wortel = _maak_bron(env, items=[
{"name": naam, "type": "str", "title": "X", "default": "x"}])
assert boilerplates.fields(wortel)[0]["secret"] is False