"""Het installatiescript: syntaxis, opties en de dingen die stuk mogen gaan. Een installer die je met `curl | sh` draait moet in elk geval syntactisch kloppen onder de shells die distributies als /bin/sh gebruiken, en niet stilzwijgend het verkeerde doen bij een typefout in de opties. """ import re import shutil import subprocess from pathlib import Path import pytest WORTEL = Path(__file__).resolve().parent.parent SCRIPT = WORTEL / "install.sh" def _draai(*args, **kw): return subprocess.run(["sh", str(SCRIPT), *args], capture_output=True, text=True, timeout=60, **kw) def test_script_bestaat_en_is_uitvoerbaar(): assert SCRIPT.is_file() assert SCRIPT.stat().st_mode & 0o111, "niet uitvoerbaar" @pytest.mark.parametrize("shell", ["sh", "dash", "bash"]) def test_syntaxis_klopt_onder(shell): """Debian en Ubuntu gebruiken dash als /bin/sh; bashismen breken daar.""" if not shutil.which(shell): pytest.skip(f"{shell} niet aanwezig") r = subprocess.run([shell, "-n", str(SCRIPT)], capture_output=True, text=True) assert r.returncode == 0, r.stderr def test_geen_bashismen(): tekst = SCRIPT.read_text(encoding="utf-8") for patroon, uitleg in [ (r"\[\[", "[[ ]] is bash-only"), (r"^\s*declare ", "declare is bash-only"), (r"\$\{[A-Za-z_]+\[", "arrays zijn bash-only"), (r"==", "== in test is bash-only"), ]: treffers = [l for l in tekst.splitlines() if re.search(patroon, l) and not l.strip().startswith("#")] assert not treffers, f"{uitleg}: {treffers[:2]}" def test_help_werkt_en_noemt_alle_opties(): r = _draai("--help") assert r.returncode == 0 for optie in ("--dir", "--port", "--bind", "--branch", "--token", "--update", "--uninstall", "--yes", "--dry-run"): assert optie in r.stdout, f"{optie} niet gedocumenteerd in --help" def test_onbekende_optie_stopt(): r = _draai("--bestaatniet") assert r.returncode != 0 assert "Onbekende optie" in r.stderr @pytest.mark.parametrize("poort", ["abc", "", "80a", "-1"]) def test_ongeldige_poort_stopt(poort): r = _draai("--port", poort) assert r.returncode != 0 # Een lege waarde valt al af bij het uitlezen van de optie, de rest bij de # controle daarna; beide meldingen gaan over de poort. assert "poort" in r.stderr.lower() or "port" in r.stderr.lower() def test_update_zonder_installatie_zegt_dat(tmp_path): r = _draai("--update", "--dir", str(tmp_path / "bestaatniet")) assert r.returncode != 0 assert "Geen installatie gevonden" in r.stderr def test_dry_run_wijzigt_niets(tmp_path): doel = tmp_path / "server-up" r = _draai("--dry-run", "--dir", str(doel), "--yes") assert not doel.exists(), "proefdraai heeft toch iets aangemaakt" assert "[droog]" in r.stdout def test_dry_run_toont_de_hele_gang_van_zaken(tmp_path): """Ook op een machine zonder Docker moet je de voorvertoning kunnen zien.""" r = _draai("--dry-run", "--dir", str(tmp_path / "su"), "--yes") for kop in ("Systeem controleren", "Broncode ophalen", "Instellingen", "Bouwen en starten"): assert kop in r.stdout, f"stap ontbreekt: {kop}" def _pad_zonder(tmp_path, *weglaten): """Een PATH met alles erin behalve de genoemde commando's.""" nep = tmp_path / "bin" nep.mkdir() import os for map_ in os.environ.get("PATH", "").split(os.pathsep): p = Path(map_) if not p.is_dir(): continue for f in p.iterdir(): if f.name in weglaten or (nep / f.name).exists(): continue try: (nep / f.name).symlink_to(f) except OSError: pass return str(nep) def test_proefdraai_werkt_zonder_curl_en_tar(tmp_path): """De CI-runner draait in python:3.12-slim, waar curl ontbreekt. Een proefdraai hoort dan nog steeds de hele voorvertoning te tonen: dat is juist het moment waarop je wil zien wat er zou gebeuren.""" r = subprocess.run(["sh", str(SCRIPT), "--dry-run", "--dir", str(tmp_path / "su"), "--yes"], capture_output=True, text=True, timeout=60, env={"PATH": _pad_zonder(tmp_path, "curl", "tar"), "HOME": str(tmp_path)}) assert r.returncode == 0, r.stderr assert "[droog]" in r.stdout assert "Bouwen en starten" in r.stdout assert not (tmp_path / "su").exists() def test_verwijst_naar_het_juiste_git_domein(): tekst = SCRIPT.read_text(encoding="utf-8") assert 'GIT_HOST="git.ramonbesselink.nl"' in tekst def test_compose_wordt_met_expliciet_bestand_aangeroepen(): """`--project-directory` alleen laat compose het bestand nog in de huidige map zoeken; met `curl | sh` is dat je thuismap.""" for regel in SCRIPT.read_text(encoding="utf-8").splitlines(): if "docker compose" in regel and ("up -d" in regel or "down " in regel): assert "-f " in regel, f"geen expliciet compose-bestand: {regel.strip()}" def test_readme_en_docs_noemen_dezelfde_installatieregel(): regel = ("curl -fsSL https://git.ramonbesselink.nl/bes-r/server-up/" "raw/branch/main/install.sh") assert regel in (WORTEL / "README.md").read_text(encoding="utf-8") assert regel in (WORTEL / "docs" / "installeren.md").read_text(encoding="utf-8")