Files
broker/tests/test_acceptance_full.py
T
coreci 7f7868f7cf docs(milestone): complete coreci-broker
---ci---
phase: 5
milestone: v0.1
status: complete
requirements:
  covered: [REQ-01, REQ-02, REQ-03, REQ-04, REQ-05, REQ-06, REQ-07, REQ-08, REQ-09, REQ-10, REQ-11, REQ-12, REQ-13, REQ-14, REQ-15, REQ-16, REQ-17, REQ-18, REQ-19, REQ-20]
  partial: []
---/ci---
2026-10-10 15:30:14 +00:00

345 lines
16 KiB
Python

import ast
import base64
import contextlib
import datetime
import importlib.util
import io
import json
import os
import sys
import tempfile
import time
import unittest
import urllib.error
import urllib.request
import uuid as uuidlib
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir)))
import coreci_broker
ADMIN_USER = "admin"
REPO_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), os.pardir))
BROKER_SOURCE_PATH = os.path.join(REPO_ROOT, "coreci_broker.py")
CLIENT_SOURCE_PATH = os.path.join(REPO_ROOT, "coreci_broker_client.py")
CLIENT_STATIC_PATH = "/static/coreci_broker_client.py"
LOGIN_PATH = "/api/broker/auth/login"
LOGOUT_PATH = "/api/broker/auth/logout"
REGISTER_PATH = "/api/broker/agents/register"
APPS_PATH = "/api/broker/app_registrations"
USERS_PATH = "/api/broker/user_allowlist"
AUDIT_PATH = "/api/broker/audit"
SECRETS_PREFIX = "/api/broker/secrets/"
PROXMOX_PATH = "coreci/proxmox"
GITEA_PATH = "coreci/gitea"
PROXMOX_SECRET = {"url": "https://pve.coreci.dev:8006", "token": "pve-tok"}
GITEA_SECRET = {"url": "https://git.coreci.dev", "token": "git-tok"}
AC_PIN_IDS = {
"AC1": "test_acceptance_p2.AcceptanceP2Tests.test_ac1_first_boot_printed_pass_login_via_ui_login_page",
"AC2": "test_acceptance_p2.AcceptanceP2Tests.test_ac2_app_registration_lifecycle_via_session_cookie",
"AC3": "test_acceptance_p2.AcceptanceP2Tests.test_ac3_user_allowlist_lifecycle_via_session_cookie",
"AC4": "test_acceptance_p3.AcceptanceP3Tests.test_ac4_derivation_identical_both_sides_stored_as_slow_hash",
"AC5": "test_acceptance_p4.AcceptanceP4Tests.test_ac5_valid_token_reads_secret_tampered_token_is_401",
"AC6": "test_acceptance_p4.AcceptanceP4Tests.test_ac6_cross_path_read_is_403_with_denied_audit",
"AC7": "test_acceptance_p3.AcceptanceP3Tests.test_ac7_revoked_or_unknown_uuid_denied_existing_token_honored",
"AC8": "test_acceptance_p3.AcceptanceP3Tests.test_ac8_unknown_or_revoked_user_denied",
"AC9": "test_acceptance_p2.AcceptanceP2Tests.test_ac9_login_and_logout_audit_lines_with_null_identity",
"AC10": "test_acceptance_p3.AcceptanceP3Tests.test_ac10_expiry_401_reregister_refreshes_expiry_and_audits",
"AC11": "test_acceptance_full.AcceptanceFullTests.test_ac11_fresh_boot_state_files_0600_umask_independent",
"AC12": "test_acceptance_p3.AcceptanceP3Tests.test_ac12_state_roundtrip_with_identity_enforcement",
"AC13": "test_acceptance_full.AcceptanceFullTests.test_ac13_client_module_fetch_byte_identical_importable_standalone",
"AC14": "test_acceptance_full.AcceptanceFullTests.test_ac14_coreci_broker_line_count_under_1400",
}
def _basic_header(password):
raw = "%s:%s" % (ADMIN_USER, password)
return {"Authorization": "Basic " + base64.b64encode(raw.encode("utf-8")).decode("ascii")}
class AcceptanceFullTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.tmp = tmp.name
self.creds_path = os.path.join(self.tmp, "creds.json")
with open(self.creds_path, "w", encoding="utf-8") as handle:
handle.write(json.dumps({PROXMOX_PATH: PROXMOX_SECRET, GITEA_PATH: GITEA_SECRET}))
def _env(self, root=None, extra=None):
root = self.tmp if root is None else root
env = {
"BROKER_ENV_PATH": os.path.join(root, ".env"),
"BROKER_DB_PATH": os.path.join(root, "broker.db"),
"BROKER_AUDIT_LOG_PATH": os.path.join(root, "broker.audit.log"),
"BROKER_CREDS_SOURCE": "file",
"BROKER_CREDS_FILE": self.creds_path,
"BROKER_UI_ROOT": os.path.join(REPO_ROOT, "coreci_broker_ui"),
"BROKER_STATIC_ROOT": REPO_ROOT,
}
env.update(extra or {})
return env
def _boot(self, env):
buffer = io.StringIO()
with contextlib.redirect_stdout(buffer):
broker = coreci_broker.boot(dict(env))
self.addCleanup(broker.audit.close)
self.addCleanup(broker.store.close)
return broker, buffer.getvalue()
def _serve(self, broker):
server = coreci_broker.serve("127.0.0.1", 0, broker)
self.addCleanup(server.server_close)
self.addCleanup(server.shutdown)
return "http://127.0.0.1:%d" % server.server_address[1]
def _printed_pass(self, output):
printed = [
line.split("=", 1)[1]
for line in output.splitlines()
if line.startswith("BROKER_ADMIN_PASS=")
]
self.assertEqual(1, len(printed))
return printed[0]
def _request(self, base, method, path, payload=None, headers=None):
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(base + path, data=data, method=method)
for key, value in (headers or {}).items():
request.add_header(key, value)
try:
with urllib.request.urlopen(request, timeout=10) as response:
return response.status, response.read(), response.headers
except urllib.error.HTTPError as error:
return error.code, error.read(), error.headers
def _request_json(self, base, method, path, payload=None, headers=None):
status, body, response_headers = self._request(base, method, path, payload, headers)
return status, json.loads(body.decode("utf-8")), response_headers
def _assert_null_identity(self, entry):
self.assertIsNone(entry["user_id"])
self.assertIsNone(entry["app_registration_uuid"])
self.assertIsNone(entry["agent_id"])
def _assert_tool_identity(self, entry, app_uuid):
self.assertEqual("ceo", entry["user_id"])
self.assertEqual(app_uuid, entry["app_registration_uuid"])
self.assertEqual("proxmox", entry["agent_id"])
def _assert_state_files_0600(self, env):
for key in ("BROKER_DB_PATH", "BROKER_AUDIT_LOG_PATH", "BROKER_ENV_PATH"):
path = env[key]
self.assertTrue(os.path.isfile(path), path)
self.assertEqual(0o600, os.stat(path).st_mode & 0o777, path)
def test_ac11_fresh_boot_state_files_0600_umask_independent(self):
env = self._env()
self._boot(env)
self._assert_state_files_0600(env)
other = tempfile.TemporaryDirectory()
self.addCleanup(other.cleanup)
other_env = self._env(root=other.name)
previous_umask = os.umask(0o777)
try:
probe = os.path.join(other.name, "umask-probe")
os.close(os.open(probe, os.O_CREAT | os.O_WRONLY, 0o666))
self.assertEqual(0o000, os.stat(probe).st_mode & 0o777)
self._boot(other_env)
finally:
os.umask(previous_umask)
self._assert_state_files_0600(other_env)
def test_ac13_client_module_fetch_byte_identical_importable_standalone(self):
broker, _ = self._boot(self._env())
base = self._serve(broker)
with open(CLIENT_SOURCE_PATH, "rb") as handle:
expected = handle.read()
with urllib.request.urlopen(base + CLIENT_STATIC_PATH, timeout=10) as response:
self.assertEqual(200, response.status)
fetched = response.read()
self.assertEqual(expected, fetched)
imports = set()
for node in ast.walk(ast.parse(fetched.decode("utf-8"))):
if isinstance(node, ast.Import):
imports.update(alias.name for alias in node.names)
elif isinstance(node, ast.ImportFrom):
imports.add(node.module or "")
self.assertTrue(imports)
for name in imports:
self.assertIn(name.split(".")[0], sys.stdlib_module_names, name)
copy_path = os.path.join(self.tmp, "fetched_coreci_broker_client.py")
with open(copy_path, "wb") as handle:
handle.write(fetched)
spec = importlib.util.spec_from_file_location("full_fetched_client", copy_path)
module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(module)
self.assertTrue(callable(module.BrokerClient))
self.assertTrue(callable(module.fetch_client_module))
for method in ("ensure_registered", "get_state", "put_state", "read_secret"):
self.assertTrue(callable(getattr(module.BrokerClient, method)), method)
def test_ac14_coreci_broker_line_count_under_1400(self):
with open(BROKER_SOURCE_PATH, "r", encoding="utf-8") as handle:
line_count = sum(1 for _ in handle)
self.assertLess(line_count, 1400)
def test_milestone_story_full_happy_path_consolidates_ac1_ac10_ac12(self):
env = self._env(extra={"BROKER_TOKEN_TTL": "1"})
first, output = self._boot(env)
printed_pass = self._printed_pass(output)
self.assertGreaterEqual(len(printed_pass), 32)
self.assertEqual(1, output.count(printed_pass))
pepper = None
with open(env["BROKER_ENV_PATH"], "r", encoding="utf-8") as handle:
for line in handle:
if line.startswith("BROKER_INSTANCE_PEPPER="):
pepper = line.split("=", 1)[1].strip()
self.assertIsNotNone(pepper)
self.assertNotIn(pepper, output)
second, silent = self._boot(env)
self.assertEqual("", silent)
second.audit.close()
second.store.close()
base = self._serve(first)
status, body, headers = self._request(base, "GET", "/ui/login")
self.assertEqual(200, status)
self.assertTrue(headers.get("Content-Type", "").startswith("text/html"))
self.assertIn('name="password"', body.decode("utf-8"))
status, body, response_headers = self._request_json(
base, "POST", LOGIN_PATH, {"username": ADMIN_USER, "password": printed_pass}
)
self.assertEqual(200, status)
self.assertEqual({"ok": True, "username": ADMIN_USER}, body)
raw_cookie = response_headers.get("Set-Cookie")
self.assertTrue(raw_cookie)
self.assertTrue(raw_cookie.startswith("broker_session="))
self.assertIn("HttpOnly", raw_cookie)
self.assertIn("SameSite=Lax", raw_cookie)
cookie = {"Cookie": raw_cookie.split(";", 1)[0]}
app_uuid = str(uuidlib.uuid4())
status, body, _ = self._request_json(
base, "POST", APPS_PATH,
{"uuid": app_uuid, "name": "proxmox-tool"}, headers=cookie,
)
self.assertEqual(201, status)
self.assertEqual({"uuid": app_uuid, "status": "active"}, body)
status, body, _ = self._request_json(
base, "POST", USERS_PATH,
{"user_id": "ceo", "email": "ceo@coreci.dev"}, headers=cookie,
)
self.assertEqual(201, status)
self.assertEqual({"user_id": "ceo", "status": "active"}, body)
with urllib.request.urlopen(base + CLIENT_STATIC_PATH, timeout=10) as response:
self.assertEqual(200, response.status)
client_source = response.read()
with open(CLIENT_SOURCE_PATH, "rb") as handle:
self.assertEqual(handle.read(), client_source)
copy_path = os.path.join(self.tmp, "story_coreci_broker_client.py")
with open(copy_path, "wb") as handle:
handle.write(client_source)
spec = importlib.util.spec_from_file_location("full_story_client", copy_path)
client_module = importlib.util.module_from_spec(spec)
spec.loader.exec_module(client_module)
client = client_module.BrokerClient(base, "proxmox", app_uuid, "ceo", ["coreci"])
state = {}
client.ensure_registered(state)
self.assertEqual(44, len(state["current_token"]))
self.assertTrue(state["current_token"].isascii())
self.assertIn("pepper", state)
self.assertEqual("proxmox", state["agent_id"])
self.assertEqual(app_uuid, state["app_registration_uuid"])
self.assertEqual("ceo", state["user_id"])
self.assertTrue(client.put_state(state))
snapshot = dict(state)
self.assertEqual(snapshot, client.get_state(state))
document = client.read_secret(PROXMOX_PATH, state)
self.assertEqual({"path", "data", "version", "updated_at"}, set(document))
self.assertEqual(PROXMOX_PATH, document["path"])
self.assertEqual(PROXMOX_SECRET, document["data"])
with self.assertRaises(client_module.BrokerError) as caught:
client.read_secret(GITEA_PATH, state)
self.assertEqual(403, caught.exception.status)
expired_at = state["token_expires_at"]
time.sleep(1.2)
document = client.read_secret(PROXMOX_PATH, state)
self.assertEqual(PROXMOX_SECRET, document["data"])
self.assertGreater(
datetime.datetime.fromisoformat(state["token_expires_at"]),
datetime.datetime.fromisoformat(expired_at),
)
status, body, _ = self._request_json(base, "POST", LOGOUT_PATH, headers=cookie)
self.assertEqual(200, status)
self.assertEqual({"ok": True}, body)
status, body, _ = self._request_json(
base, "GET", AUDIT_PATH + "?limit=1000", headers=_basic_header(printed_pass)
)
self.assertEqual(200, status)
self.assertIsNone(body["next_since"])
entries = body["entries"]
def select(action, result=None):
return [
entry for entry in entries
if entry["action"] == action and (result is None or entry["result"] == result)
]
logins = select("login", "ok")
self.assertEqual(1, len(logins))
self.assertEqual(LOGIN_PATH, logins[0]["path"])
self._assert_null_identity(logins[0])
add_apps = select("add_app", "ok")
self.assertEqual(1, len(add_apps))
self.assertEqual(APPS_PATH + "/" + app_uuid, add_apps[0]["path"])
self._assert_null_identity(add_apps[0])
add_users = select("add_user", "ok")
self.assertEqual(1, len(add_users))
self.assertEqual(USERS_PATH + "/ceo", add_users[0]["path"])
self._assert_null_identity(add_users[0])
registers = select("register", "ok")
self.assertGreaterEqual(len(registers), 2)
for entry in registers:
self.assertEqual(REGISTER_PATH, entry["path"])
self._assert_tool_identity(entry, app_uuid)
ok_reads = select("read", "ok")
self.assertGreaterEqual(len(ok_reads), 2)
for entry in ok_reads:
self.assertEqual(PROXMOX_PATH, entry["path"])
self._assert_tool_identity(entry, app_uuid)
denied_reads = select("read", "denied")
self.assertEqual(1, len(denied_reads))
self.assertEqual(GITEA_PATH, denied_reads[0]["path"])
self._assert_tool_identity(denied_reads[0], app_uuid)
auth_denied = [
entry for entry in select("denied", "denied")
if entry["path"] == SECRETS_PREFIX + PROXMOX_PATH
]
self.assertGreaterEqual(len(auth_denied), 1)
self.assertEqual(app_uuid, auth_denied[0]["app_registration_uuid"])
self.assertIsNone(auth_denied[0]["user_id"])
self.assertIsNone(auth_denied[0]["agent_id"])
logouts = select("logout", "ok")
self.assertEqual(1, len(logouts))
self.assertEqual(LOGOUT_PATH, logouts[0]["path"])
self._assert_null_identity(logouts[0])
def test_coverage_tally_pins_ac1_through_ac14_across_suite(self):
self.assertEqual(14, len(AC_PIN_IDS))
loader = unittest.TestLoader()
suite = loader.discover(os.path.dirname(os.path.abspath(__file__)))
stack, ids = [suite], set()
while stack:
item = stack.pop()
if isinstance(item, unittest.TestSuite):
stack.extend(item)
else:
ids.add(item.id())
for ac in sorted(AC_PIN_IDS):
with self.subTest(ac=ac):
self.assertIn(AC_PIN_IDS[ac], ids)
if __name__ == "__main__":
unittest.main()