#!/usr/bin/env python3 """Tests for every hcp-run-guard refusal, exit-0, discard, and reconcile case.""" from __future__ import annotations import importlib.util from pathlib import Path from typing import Any SCRIPT = Path(__file__).with_name("hcp-run-guard.py") WORKSPACE = "shoc-frontend-new-dev" PATTERNS = [ "terraform/live/dev/**", "terraform/live/modules/**", ] def load_module(): spec = importlib.util.spec_from_file_location("hcp_run_guard", SCRIPT) module = importlib.util.module_from_spec(spec) assert spec.loader is not None spec.loader.exec_module(module) return module def workspace_payload( *, auto_apply: bool = False, speculative: bool = True, tags_regex: str | None = None, trigger_patterns: list[str] | None = None, locked: bool = False, current_run: dict[str, Any] | None = None, ) -> dict[str, Any]: return { "data": { "attributes": { "auto-apply": auto_apply, "speculative-enabled": speculative, "vcs-repo": {"tags-regex": tags_regex}, "trigger-patterns": PATTERNS if trigger_patterns is None else trigger_patterns, "locked": locked, }, "relationships": { "current-run": {"data": current_run}, }, } } def run_payload(*, status: str, plan_only: bool = False) -> dict[str, Any]: return {"data": {"attributes": {"status": status, "plan-only": plan_only}}} def check(module, payloads: dict[str, Any], posts: list | None = None): calls: list[str] = [] def get(url: str) -> dict[str, Any]: calls.append(url) if url not in payloads: raise AssertionError(f"unexpected GET {url}") return payloads[url] recorded: list[tuple[str, dict[str, Any]]] = [] def post(url: str, payload: dict[str, Any]) -> int: recorded.append((url, payload)) if posts: return posts.pop(0) return 202 try: code = module.check_and_discard( workspace=WORKSPACE, token="test-token", get=get, post=post, ) return code, None, calls, recorded except module.GuardError as exc: return 1, str(exc), calls, recorded def reconcile(module, outcome: str, payloads: dict[str, Any], run_id: str = "run-1"): def get(url: str) -> dict[str, Any]: if url not in payloads: raise AssertionError(f"unexpected GET {url}") return payloads[url] try: code = module.reconcile_apply( run_id=run_id, apply_outcome=outcome, token="test-token", get=get, ) return code, None except module.GuardError as exc: return 1, str(exc) def main() -> int: module = load_module() ws = f"https://app.terraform.io/api/v2/organizations/seahaven/workspaces/{WORKSPACE}" run_url = "https://app.terraform.io/api/v2/runs/run-1" discard_url = f"{run_url}/actions/discard" failures: list[str] = [] def expect_refuse(name: str, payloads: dict[str, Any], fragment: str) -> None: code, error, _, recorded = check(module, payloads) if code != 1 or not error or fragment not in error: failures.append(f"{name}: expected refuse containing {fragment!r}, got {code} {error}") if recorded: failures.append(f"{name}: discard was posted on a refusal") expect_refuse( "auto-apply", {ws: workspace_payload(auto_apply=True)}, "auto-apply is on", ) expect_refuse( "speculative-off", {ws: workspace_payload(speculative=False)}, "speculative plans are off", ) expect_refuse( "tags-regex", {ws: workspace_payload(tags_regex="^v")}, "tag-based VCS triggering", ) expect_refuse( "wrong-patterns", {ws: workspace_payload(trigger_patterns=["terraform/**"])}, "trigger-patterns must be", ) expect_refuse( "locked-without-run", {ws: workspace_payload(locked=True, current_run=None)}, "locked without a current run", ) expect_refuse( "applying", { ws: workspace_payload(locked=True, current_run={"id": "run-1"}), run_url: run_payload(status="applying"), }, "wait, do not discard an apply", ) expect_refuse( "not-discardable", { ws: workspace_payload(locked=True, current_run={"id": "run-1"}), run_url: run_payload(status="errored"), }, "is not discardable", ) code, error, _, recorded = check(module, {ws: workspace_payload(locked=False)}) if code != 0 or error is not None or recorded: failures.append(f"unlocked: expected exit 0, got {code} {error} {recorded}") code, error, _, recorded = check( module, { ws: workspace_payload(locked=True, current_run={"id": "run-1"}), run_url: run_payload(status="planned", plan_only=True), }, ) if code != 0 or recorded: failures.append(f"plan-only: expected exit 0 without discard, got {code} {recorded}") code, error, _, recorded = check( module, { ws: workspace_payload(locked=True, current_run={"id": "run-1"}), run_url: run_payload(status="planned"), }, ) if code != 0 or error is not None: failures.append(f"discard: expected exit 0, got {code} {error}") if not recorded or recorded[0][0] != discard_url: failures.append(f"discard: posted {recorded}") code, error, _, recorded = check( module, { ws: workspace_payload(locked=True, current_run={"id": "run-1"}), run_url: run_payload(status="policy_checked"), }, posts=[409], ) if code != 0: failures.append(f"discard-409: expected exit 0, got {code} {error}") try: module.check_and_discard(workspace=WORKSPACE, token="", get=lambda _url: {}) failures.append("missing-token: accepted empty token") except module.GuardError: pass code, error = reconcile(module, "success", {}) if code != 0: failures.append(f"reconcile-success: expected 0, got {code} {error}") code, error = reconcile( module, "failure", {run_url: run_payload(status="applied")}, ) if code != 0: failures.append(f"reconcile-applied: expected 0, got {code} {error}") code, error = reconcile( module, "failure", {run_url: run_payload(status="errored")}, ) if code != 1 or not error or "errored" not in error: failures.append(f"reconcile-errored: expected refuse, got {code} {error}") try: module.reconcile_apply(run_id="", apply_outcome="failure", token="test-token") failures.append("reconcile-missing-run: accepted empty run id") except module.GuardError: pass try: module.reconcile_apply(run_id="run-1", apply_outcome="failure", token="") failures.append("reconcile-missing-token: accepted empty token") except module.GuardError: pass if failures: print("FAIL: hcp-run-guard cases failed", file=__import__("sys").stderr) for item in failures: print(f" - {item}", file=__import__("sys").stderr) return 1 print("PASS: HCP run guard checks") return 0 if __name__ == "__main__": raise SystemExit(main())