server-up/tests/test_boilerplates.py
Ramon b8c8e89fa0
All checks were successful
Deploy server-up (dev) / deploy (push) Successful in 1m5s
v0.6.00-beta - 29 apps erbij, en named volumes die verdwenen
Bij het toevoegen van de apps kwam een bestaande fout boven water.
_drop_empty_mappings ruimt sleutels op die een <% if %>-blok leeg achterlaat,
maar een named volume wordt gedeclareerd als een kale sleutel zonder waarde
(`  immich_pgdata:`). Die werd opgeruimd, waarna ook het bovenliggende
`volumes:` wegviel en de services verwezen naar een volume dat nergens meer
gedeclareerd stond. Immich, Ghost, Miniflux, Paperless-ngx, Unifi Network en
Vikunja waren daardoor niet installeerbaar: docker compose weigert met "refers
to undefined volume". Alleen bekende compose-sleutels worden nu opgeruimd.

29 nieuwe apps (teller op 65):
- Domotica: zigbee2mqtt, mosquitto, esphome, node-red, zwave-js-ui,
  homebridge, scrypted, frigate
- 3D-printen: bambuddy, octoprint, spoolman
- Gezin/huishouden: baby-buddy, grocy, tandoor, firefly-iii
- Media: audiobookshelf, navidrome, kavita, calibre-web, jellyseerr
- Kennis: bookstack, freshrss, wallabag
- Netwerk/beheer: wg-easy, homepage, searxng, duplicati, watchtower,
  technitium-dns

Nieuwe tests/test_apps.py rendert elke app met de echte engine en controleert
geldige YAML, image of build per service, gedeclareerde named volumes, geen
onvervangen variabelen en geen onbedoeld dubbele standaardpoorten. Die laatste
vond zeven botsingen tussen nieuwe en bestaande apps; rechtgezet. Logische
botsingen (reverse proxies op 80/443, dns-blokkers op 53) staan met uitleg in
een allowlist.

Het image van Bambuddy is nagetrokken op de bron
(github.com/maziggy/bambuddy): ghcr.io/maziggy/bambuddy, poort 8000.

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

105 lines
4 KiB
Python

"""Boilerplate-rendering: de sandbox, en de YAML-opruiming die conditionele
blokken achterlaten."""
import json
import pytest
import sys
from pathlib import Path
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "server-up"))
from core import boilerplates as bp
def _maak_boilerplate(tmp_path, compose: str, variables=None) -> Path:
d = tmp_path / "stack"
(d / "files").mkdir(parents=True)
(d / "template.json").write_text(json.dumps({
"kind": "compose",
"metadata": {"name": "Test", "description": "", "tags": []},
"variables": variables if variables is not None else [{
"title": "Algemeen",
"items": [
{"name": "service_name", "type": "str", "default": "test"},
{"name": "port", "type": "int", "default": 8080},
{"name": "admin", "type": "bool", "default": False},
],
}],
}), encoding="utf-8")
(d / "files" / "compose.yaml").write_text(compose, encoding="utf-8")
return d
def test_render_vult_variabelen_in(tmp_path):
src = _maak_boilerplate(tmp_path, "services:\n << service_name >>:\n image: nginx\n"
" ports:\n - \"<< port >>:80\"\n")
dest = tmp_path / "out"
bp.render_to_dir(src, dest, {"service_name": "web", "port": 9090})
tekst = (dest / "docker-compose.yml").read_text()
assert "web:" in tekst
assert '"9090:80"' in tekst
def test_conditioneel_blok_wordt_overgeslagen(tmp_path):
src = _maak_boilerplate(
tmp_path,
"services:\n app:\n image: nginx\n"
" environment:\n"
"<% if admin %>\n - ADMIN=1\n<% endif %>\n")
dest = tmp_path / "out"
bp.render_to_dir(src, dest, {"admin": False})
tekst = (dest / "docker-compose.yml").read_text()
assert "ADMIN" not in tekst
# `environment:` zonder inhoud is ongeldige YAML en moet weg zijn.
assert "environment:" not in tekst
def test_lege_mapping_aan_einde_verdwijnt():
assert bp._drop_empty_mappings("services:\n app:\n image: x\n volumes:\n") \
== "services:\n app:\n image: x\n"
def test_gevulde_mapping_blijft_staan():
tekst = "services:\n app:\n image: x\n"
assert bp._drop_empty_mappings(tekst) == tekst
def test_sandbox_geeft_geen_python_internals_prijs(tmp_path):
"""Templates komen uit externe git-repo's en worden al gerenderd in
/api/store/preview. De sandbox levert Undefined (leeg) voor afgeschermde
attributen, zodat er nooit een echt Python-object in het resultaat komt."""
src = _maak_boilerplate(tmp_path, "x: << service_name.__class__ >>\n")
bp.render_to_dir(src, tmp_path / "out", {"service_name": "web"})
tekst = (tmp_path / "out" / "docker-compose.yml").read_text()
# De waarde blijft leeg; `x:` zelf blijft staan omdat alleen bekende
# compose-sleutels worden opgeruimd (zie _OPRUIMBARE_SLEUTELS).
assert tekst.strip() == "x:"
assert "class" not in tekst
assert "object" not in tekst
def test_sandbox_laat_aanroep_van_internals_falen(tmp_path):
"""De klassieke SSTI-keten faalt hard in plaats van de subclass-lijst —
en daarmee een weg naar os.system — op te leveren."""
src = _maak_boilerplate(
tmp_path, "x: << ''.__class__.__mro__[1].__subclasses__() >>\n")
with pytest.raises(bp.BoilerplateError):
bp.render_to_dir(src, tmp_path / "out", {})
def test_normale_filters_blijven_werken(tmp_path):
"""De sandbox mag de bestaande templates niet breken — vaultwarden gebruikt
bijvoorbeeld `<< signups_allowed | lower >>`."""
src = _maak_boilerplate(
tmp_path, "a: << service_name | upper >>\nb: << admin | lower >>\n")
bp.render_to_dir(src, tmp_path / "out", {"service_name": "web", "admin": True})
tekst = (tmp_path / "out" / "docker-compose.yml").read_text()
assert "a: WEB" in tekst
assert "b: true" in tekst
def test_is_boilerplate_herkent_layout(tmp_path):
src = _maak_boilerplate(tmp_path, "services: {}\n")
assert bp.is_boilerplate(src)
assert not bp.is_boilerplate(tmp_path)