"""Boilerplate-rendering: de sandbox, en de YAML-opruiming die conditionele blokken achterlaten.""" import json import stat 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) def test_architectuurmetadata_wordt_genormaliseerd(tmp_path): src = _maak_boilerplate(tmp_path, "services: {}\n") pad = src / "template.json" data = json.loads(pad.read_text(encoding="utf-8")) data["metadata"]["architectures"] = ["x86_64", "aarch64", "arm64", "onbekend"] data["metadata"]["architecture_note"] = "Gebruik een 64-bits OS." pad.write_text(json.dumps(data), encoding="utf-8") meta = bp.metadata(src) assert meta["architectures"] == ["amd64", "arm64"] assert meta["architecture_note"] == "Gebruik een 64-bits OS." def test_explicitiete_architectuur_mismatch_wordt_geweigerd(): meta = {"architectures": ["amd64", "arm64"], "architecture_note": "Gebruik een 64-bits OS."} assert bp.compatibility(meta, "aarch64")["supported"] is True fout = bp.compatibility(meta, "armv7l") assert fout["known"] is True assert fout["supported"] is False assert "ARM 32-bit" in fout["reason"] assert "64-bits OS" in fout["reason"] def test_onbekende_architectuurmetadata_blokkeert_niet(): assert bp.compatibility({}, "armv7l")["supported"] is True assert bp.compatibility({"architectures": ["vreemd"]}, "armv7l")["supported"] is True def test_render_behoudt_uitvoerbaar_recht_van_tekstscript(tmp_path): src = _maak_boilerplate(tmp_path, "services: {}\n") script = src / "files" / "init.sh" script.write_text("#!/bin/sh\necho << service_name >>\n", encoding="utf-8") script.chmod(0o755) dest = tmp_path / "out" bp.render_to_dir(src, dest, {"service_name": "test"}) assert (dest / "init.sh").stat().st_mode & stat.S_IXUSR