From 35a1151a7dbf7c8792131b31fa690c84dfbb2f9b Mon Sep 17 00:00:00 2001 From: Danny Groenewegen Date: Mon, 31 Aug 2026 08:52:20 +0200 Subject: [PATCH] test(local-scripts): add tests for local script hooks --- tests/test_local_scripts.py | 184 ++++++++++++++++++++++++++++++++++++ tests/test_strict_hooks.py | 33 +++++++ 2 files changed, 217 insertions(+) create mode 100644 tests/test_local_scripts.py diff --git a/tests/test_local_scripts.py b/tests/test_local_scripts.py new file mode 100644 index 0000000..b20a810 --- /dev/null +++ b/tests/test_local_scripts.py @@ -0,0 +1,184 @@ +"""Tests for local script functionality in hooks.""" + +import logging +import os +import stat +import sys + +import click +import pytest +from click.testing import CliRunner + +sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__)))) + +import alakazam +from alakazam import (create_secrets, deploy_apps, execute_cmds, get_abra_dir, + resolve_path, run_local_script, run_secret_hooks) + + +class FakeProcess: + def __init__(self, returncode): + self.returncode = returncode + + +def make_script(tmp_path, name="script.sh", executable=True): + """Write a script file under tmp_path, executable by default.""" + path = tmp_path / name + path.write_text("#!/bin/sh\nexit 0\n") + if executable: + path.chmod(path.stat().st_mode | stat.S_IXUSR) + return path + + +class TestResolvePath: + def test_an_absolute_path_passes_through_unchanged(self, tmp_path): + absolute = tmp_path / "script.sh" + assert resolve_path(str(absolute)) == absolute + + def test_a_tilde_path_expands_against_home(self, monkeypatch, tmp_path): + monkeypatch.setenv("HOME", str(tmp_path)) + assert resolve_path("~/script.sh") == tmp_path / "script.sh" + + def test_a_relative_path_resolves_against_the_given_base(self, tmp_path): + base = tmp_path / "instance" + assert resolve_path("scripts/script.sh", base) == base / "scripts/script.sh" + + def test_a_relative_path_resolves_against_root_path_by_default(self, monkeypatch, tmp_path): + monkeypatch.setattr(alakazam, "ROOT_PATH", tmp_path) + assert resolve_path("scripts/script.sh") == tmp_path / "scripts/script.sh" + + +class TestRunLocalScript: + def test_the_script_receives_its_env_vars_and_arguments(self, monkeypatch, tmp_path): + script = make_script(tmp_path) + calls = [] + monkeypatch.setattr(alakazam.subprocess, "run", + lambda cmd, env: calls.append((cmd, env)) or FakeProcess(0)) + run_local_script(["script", str(script), "arg1"], "login.a.org", "a.org", "a.org") + [(cmd, env)] = calls + assert cmd == [str(script), "arg1"] + assert env["ALAKAZAM_APP_DOMAIN"] == "login.a.org" + assert env["ALAKAZAM_APP_SERVER"] == "a.org" + assert env["ALAKAZAM_INSTANCE_DOMAIN"] == "a.org" + + def test_a_non_executable_script_is_not_run(self, monkeypatch, tmp_path, caplog): + script = make_script(tmp_path, executable=False) + monkeypatch.setattr(alakazam.subprocess, "run", + lambda *a, **k: pytest.fail("a non-executable script must not run")) + with caplog.at_level(logging.ERROR): + run_local_script(["script", str(script)], "login.a.org", "a.org", "a.org") + assert "not executable" in caplog.text + + def test_dry_run_does_not_execute_the_script(self, monkeypatch, tmp_path, capsys): + script = make_script(tmp_path) + monkeypatch.setattr(alakazam.subprocess, "run", + lambda *a, **k: pytest.fail("dry_run must not execute anything")) + run_local_script(["script", str(script)], "login.a.org", "a.org", "a.org", dry_run=True) + assert "Run local script" in capsys.readouterr().out + + def test_a_non_zero_exit_only_warns_by_default(self, monkeypatch, tmp_path, caplog): + script = make_script(tmp_path) + monkeypatch.setattr(alakazam.subprocess, "run", lambda *a, **k: FakeProcess(1)) + with caplog.at_level(logging.WARNING): + run_local_script(["script", str(script)], "login.a.org", "a.org", "a.org") + assert "exited with code 1" in caplog.text + + def test_a_non_zero_exit_aborts_in_strict_mode(self, monkeypatch, tmp_path): + script = make_script(tmp_path) + monkeypatch.setattr(alakazam.subprocess, "run", lambda *a, **k: FakeProcess(1)) + with pytest.raises(click.ClickException) as excinfo: + run_local_script(["script", str(script)], "login.a.org", "a.org", "a.org", strict=True) + assert "exited with code 1" in excinfo.value.message + + def test_a_non_executable_script_aborts_in_strict_mode(self, monkeypatch, tmp_path): + script = make_script(tmp_path, executable=False) + monkeypatch.setattr(alakazam.subprocess, "run", + lambda *a, **k: pytest.fail("a non-executable script must not run")) + with pytest.raises(click.ClickException) as excinfo: + run_local_script(["script", str(script)], "login.a.org", "a.org", "a.org", strict=True) + assert "not executable" in excinfo.value.message + + +class TestScriptHookDispatch: + def test_a_script_secret_hook_dispatches_to_run_local_script(self, monkeypatch, tmp_path): + script = make_script(tmp_path) + calls = [] + monkeypatch.setattr(alakazam, "run_local_script", lambda *a, **k: calls.append((a, k))) + monkeypatch.setattr(alakazam, "abra", lambda *a, **k: pytest.fail("should not call abra")) + run_secret_hooks("login.a.org", {"secret_hooks": [f"script {script}"], "server": "a.org"}, "a.org") + [(args, kwargs)] = calls + assert args == (["script", str(script)], "login.a.org", "a.org", "a.org") + + def test_a_normal_secret_hook_goes_through_abra(self, monkeypatch): + calls = [] + monkeypatch.setattr(alakazam, "abra", lambda *a, **k: calls.append((a, k)) or "") + run_secret_hooks("login.a.org", {"secret_hooks": ["insert_cert"], "server": "a.org"}) + assert calls, "a non-script hook must still run through abra" + + def test_a_script_command_dispatches_to_run_local_script(self, monkeypatch, tmp_path): + script = make_script(tmp_path) + calls = [] + monkeypatch.setattr(alakazam, "run_local_script", lambda *a, **k: calls.append((a, k))) + monkeypatch.setattr(alakazam, "abra", lambda *a, **k: pytest.fail("should not call abra")) + execute_cmds({"app_domain": "login.a.org", "server": "a.org", + "initial-hooks": [f"script {script} arg1"]}, initial=True) + [(args, kwargs)] = calls + assert args == (["script", str(script), "arg1"], "login.a.org", "a.org", "", False, False) + + def test_a_normal_command_goes_through_abra(self, monkeypatch): + calls = [] + monkeypatch.setattr(alakazam, "abra", lambda *a, **k: calls.append((a, k)) or "") + execute_cmds({"app_domain": "login.a.org", "server": "a.org", + "initial-hooks": ["app set_default_quota"]}, initial=True) + assert calls, "a non-script command must still run through abra" + + def test_a_script_token_with_a_missing_file_falls_back_to_abra(self, monkeypatch, tmp_path): + """A 'script' hook whose path does not exist is not a script hook at all, just an abra + command whose container happens to be named 'script'.""" + missing = tmp_path / "does-not-exist.sh" + calls = [] + monkeypatch.setattr(alakazam, "run_local_script", + lambda *a, **k: pytest.fail("should not run a missing script")) + monkeypatch.setattr(alakazam, "abra", lambda *a, **k: calls.append((a, k)) or "") + execute_cmds({"app_domain": "login.a.org", "server": "a.org", + "initial-hooks": [f"script {missing}"]}, initial=True) + assert calls, "a missing script path must fall back to a normal abra command" + + +class TestInstanceDomainThreading: + """create_secrets(), deploy_apps() and the 'cmd' CLI command each thread instance_domain + into run_secret_hooks()/execute_cmds() for script hooks.""" + + @pytest.fixture + def app_config(self, monkeypatch): + """A single 'authentik' app on instance 'a.org', shared by tests below.""" + config = {"app_domain": "login.a.org", "server": "a.org"} + monkeypatch.setattr(alakazam, "INSTANCE_CONFIGS", {"a.org": {"authentik": config}}) + return config + + def test_create_secrets_threads_the_instance_domain(self, monkeypatch, app_config): + calls = [] + monkeypatch.setattr(alakazam, "run_secret_hooks", lambda *a, **k: calls.append((a, k))) + create_secrets(recipes=(), variants={"secret-hooks"}) + [(args, kwargs)] = calls + assert args == ("login.a.org", app_config, "a.org") + + def test_deploy_apps_threads_the_instance_domain(self, monkeypatch, app_config): + app_config["version"] = "1.0.0" + monkeypatch.setattr(alakazam, "abra", lambda *a, **k: "") + calls = [] + monkeypatch.setattr(alakazam, "execute_cmds", lambda app_config, **k: calls.append(k)) + deploy_apps({"a.org": [["authentik", "login.a.org"]]}, execute_hooks=True) + [kwargs] = calls + assert kwargs["deploy"] is True + assert kwargs["instance_domain"] == "a.org" + + def test_cmd_threads_the_instance_domain(self, monkeypatch, app_config): + monkeypatch.setattr(alakazam, "get_deployed_apps", lambda apps: {"login.a.org": "1.0.0"}) + calls = [] + monkeypatch.setattr(alakazam, "execute_cmds", lambda app_config, **k: calls.append(k)) + result = CliRunner().invoke(alakazam.cmd, ["-i"], standalone_mode=False) + assert result.exception is None + [kwargs] = calls + assert kwargs["initial"] is True + assert kwargs["instance_domain"] == "a.org" diff --git a/tests/test_strict_hooks.py b/tests/test_strict_hooks.py index 41a2b96..8d5c842 100644 --- a/tests/test_strict_hooks.py +++ b/tests/test_strict_hooks.py @@ -1,6 +1,7 @@ """Tests for the strict hook mode that lets a scratch build fail on a broken hook.""" import os +import stat import sys import click @@ -12,6 +13,14 @@ import alakazam from alakazam import execute_cmds, run_secret_hooks +def failing_script(tmp_path): + """A tiny local script that exits non-zero, for testing 'script' hook failure handling.""" + script = tmp_path / "fail.sh" + script.write_text("#!/bin/sh\nexit 1\n") + script.chmod(script.stat().st_mode | stat.S_IXUSR) + return script + + class TestStrictHooks: def test_a_secret_hook_failure_is_tolerated_by_default(self, monkeypatch): """'secrets' has always continued past a failing hook, that must not change.""" @@ -39,3 +48,27 @@ class TestStrictHooks: with pytest.raises(click.ClickException) as excinfo: execute_cmds({"app_domain": "login.a.org", "server": "a.org", "initial-hooks": ["app init"]}, initial=True, strict=True) assert "command 'app init' failed" in excinfo.value.message + + def test_a_script_secret_hook_failure_is_tolerated_by_default(self, tmp_path): + """A failing local script must be tolerated by default too, same as a failing abra.sh hook.""" + script = failing_script(tmp_path) + run_secret_hooks("login.a.org", {"secret_hooks": [f"script {script}"], "server": "a.org"}) + + def test_a_script_secret_hook_failure_aborts_in_strict_mode(self, tmp_path): + script = failing_script(tmp_path) + with pytest.raises(click.ClickException) as excinfo: + run_secret_hooks("login.a.org", {"secret_hooks": [f"script {script}"], "server": "a.org"}, strict=True) + assert str(script) in excinfo.value.message + assert "failed" in excinfo.value.message + + def test_a_script_command_failure_is_tolerated_by_default(self, tmp_path): + script = failing_script(tmp_path) + execute_cmds({"app_domain": "login.a.org", "server": "a.org", "initial-hooks": [f"script {script}"]}, initial=True) + + def test_a_script_command_failure_aborts_in_strict_mode(self, tmp_path): + script = failing_script(tmp_path) + with pytest.raises(click.ClickException) as excinfo: + execute_cmds({"app_domain": "login.a.org", "server": "a.org", "initial-hooks": [f"script {script}"]}, + initial=True, strict=True) + assert str(script) in excinfo.value.message + assert "failed" in excinfo.value.message