net-kingdom/bounded-executor/tests/test_clients.py
tegwick 15c9cdbef8 NK-WP-0056-T10: wait for OpenBao reachability before the first login
First live journey Job was refused at setup (URLError). netcheck showed every
connection refused at 0 s and open from 3 s: k3s applies NetworkPolicy to a
new pod a moment late. Both CLI modes now poll OpenBao's unauthenticated
sys/health for up to 30 s before logging in; only connection errors retry.
48 executor tests.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

Assistant: claude-code
Assistant-Model: opus
Assistant-Process: 5502@bnt-lap001
Assistant-Session: 68b3b02f-5d0f-4e71-8421-796f62e50196
2026-10-31 20:33:17 +01:00

133 lines
5.5 KiB
Python

"""approval-engine and audit-core clients, and the CLI's setup failure path (NK-WP-0056-T13). Fakes only."""
from __future__ import annotations
import importlib.util
import json
import pathlib
import sys
import tempfile
import threading
import unittest
from http.server import BaseHTTPRequestHandler, ThreadingHTTPServer
HERE = pathlib.Path(__file__).resolve().parent
def load(name):
spec = importlib.util.spec_from_file_location(name, HERE.parent / f"{name}.py")
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
C = load("clients")
CLI = load("cli")
class Fake:
def __init__(self):
self.requests = []
self.status = 200
def serve(fake):
class H(BaseHTTPRequestHandler):
def handle_one(self, method):
raw = self.rfile.read(int(self.headers.get("Content-Length", "0") or 0))
self.server_fake.requests.append((method, self.path, dict(self.headers), json.loads(raw) if raw else None))
body = json.dumps({"id": "apr-1", "status": "approved"} if method == "GET" else
{"status": "consumed", "idempotent": False}).encode()
self.send_response(self.server_fake.status)
self.send_header("Content-Type", "application/json")
self.send_header("Content-Length", str(len(body)))
self.end_headers()
self.wfile.write(body)
def do_GET(self):
self.handle_one("GET")
def do_POST(self):
self.handle_one("POST")
def log_message(self, *a):
return
H.server_fake = fake
server = ThreadingHTTPServer(("127.0.0.1", 0), H)
threading.Thread(target=server.serve_forever, kwargs={"poll_interval": 0.02}, daemon=True).start()
return server
class ClientTests(unittest.TestCase):
def setUp(self):
self.fake = Fake()
self.server = serve(self.fake)
self.addCleanup(self.server.shutdown)
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.token = pathlib.Path(tmp.name) / "token"
self.token.write_text("synthetic-bearer\n")
self.base = f"http://127.0.0.1:{self.server.server_port}"
def test_approval_engine_get_and_consume(self):
ae = C.ApprovalEngine(self.base, str(self.token))
self.assertEqual(ae.get("apr-1")["status"], "approved")
ae.consume("apr-1", "sha256:" + "a" * 64, "run-1")
(_, p1, h1, _), (_, p2, _, b2) = self.fake.requests
self.assertEqual((p1, p2), ("/v1/approvals/apr-1", "/v1/approvals/apr-1/consume"))
self.assertEqual(h1["Authorization"], "Bearer synthetic-bearer")
self.assertEqual(b2, {"request_digest": "sha256:" + "a" * 64, "decision_id": "run-1"})
def test_approval_id_cannot_escape_the_path(self):
ae = C.ApprovalEngine(self.base, str(self.token))
for bad in ("../admin", "", "a/b"):
with self.subTest(bad=bad), self.assertRaises(C.ClientError):
ae.get(bad)
self.assertEqual(self.fake.requests, [])
def test_audit_core_idempotency_key_equals_event_id(self):
event = {"id": "run:r1:started", "type": "execution.run.started", "source": "s", "subject": "x",
"tenant": "tenant:platform", "correlation_id": "r1", "occurred_at": "2026-11-03T09:30:00+00:00",
"data": {"a": 1}}
C.AuditCore(self.base, str(self.token)).emit(event)
((method, path, headers, body),) = self.fake.requests
self.assertEqual((method, path, headers["Idempotency-Key"]), ("POST", "/v1/events", "run:r1:started"))
self.assertEqual(body, event)
def test_errors_carry_status_only(self):
self.fake.status = 403
with self.assertRaises(C.ClientError) as caught:
C.ApprovalEngine(self.base, str(self.token)).get("apr-1")
self.assertEqual(str(caught.exception), "GET 403")
class CliTests(unittest.TestCase):
def test_exit_codes(self):
self.assertEqual(CLI.exit_code({"outcome": "completed"}), 0)
self.assertEqual(CLI.exit_code({"outcome": "reverted"}), 3)
self.assertEqual(CLI.exit_code({"outcome": "refused", "reason": "approval_entry"}), 4)
self.assertEqual(CLI.exit_code({"outcome": "revert_failed"}), 5)
self.assertEqual(CLI.exit_code({"outcome": "stopped", "reason": "audit_unavailable"}), 6)
self.assertEqual(CLI.exit_code({"outcome": "completed", "reason": "receipt_not_in_custody"}), 6)
def test_setup_failure_refuses_before_any_effect(self):
with tempfile.TemporaryDirectory() as tmp:
root = pathlib.Path(tmp)
(root / "jwt").write_text("synthetic-jwt")
cfg = {"openbao": {"addr": "http://127.0.0.1:9", "k8s_role": "r", "jwt_file": str(root / "jwt")}}
(root / "cfg.json").write_text(json.dumps(cfg))
admission = CLI.X.REPO / "examples/bounded-execution-admission/bind-rotation-svc-authelia.yaml"
import contextlib, io
wait = CLI.OPS.wait_reachable
self.addCleanup(setattr, CLI.OPS, "wait_reachable", wait)
CLI.OPS.wait_reachable = lambda addr, **kw: wait(addr, **{**kw, "timeout": 0.2, "interval": 0.1})
with contextlib.redirect_stdout(io.StringIO()) as out:
code = CLI.main(["--admission", str(admission), "--approval-id", "apr-1", "--config", str(root / "cfg.json")])
self.assertEqual(code, 4)
self.assertEqual(json.loads(out.getvalue())["reason"].split(":")[0], "setup")
if __name__ == "__main__":
unittest.main()