Files
broker/tests/test_apps.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

187 lines
7.8 KiB
Python

import base64
import contextlib
import io
import json
import os
import sys
import tempfile
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"
ADMIN_PASS = "correct-horse-battery-staple"
BASE_PATH = "/api/broker/app_registrations"
def _basic_header(username, password):
raw = "%s:%s" % (username, password)
return "Basic " + base64.b64encode(raw.encode("utf-8")).decode("ascii")
class AppRegistrationsTests(unittest.TestCase):
def setUp(self):
tmp = tempfile.TemporaryDirectory()
self.addCleanup(tmp.cleanup)
self.tmp = tmp.name
self.env = {
"BROKER_ENV_PATH": os.path.join(self.tmp, ".env"),
"BROKER_DB_PATH": os.path.join(self.tmp, "broker.db"),
"BROKER_AUDIT_LOG_PATH": os.path.join(self.tmp, "broker.audit.log"),
"BROKER_ADMIN_USER": ADMIN_USER,
"BROKER_ADMIN_PASS": ADMIN_PASS,
}
with contextlib.redirect_stdout(io.StringIO()):
self.broker = coreci_broker.boot(dict(self.env))
self.addCleanup(self.broker.audit.close)
self.addCleanup(self.broker.store.close)
self.server = coreci_broker.serve("127.0.0.1", 0, self.broker)
self.addCleanup(self.server.server_close)
self.addCleanup(self.server.shutdown)
self.base = "http://127.0.0.1:%d" % self.server.server_address[1]
self.admin = {"Authorization": _basic_header(ADMIN_USER, ADMIN_PASS)}
def _request(self, method, path, payload=None, headers=None):
data = None if payload is None else json.dumps(payload).encode("utf-8")
request = urllib.request.Request(self.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=5) as response:
return response.status, json.loads(response.read().decode("utf-8"))
except urllib.error.HTTPError as error:
return error.code, json.loads(error.read().decode("utf-8"))
def _add(self, app_uuid, name=None, headers=None):
payload = {"uuid": app_uuid}
if name is not None:
payload["name"] = name
return self._request("POST", BASE_PATH, payload, headers or self.admin)
def _row(self, app_uuid):
status, body = self._request("GET", BASE_PATH, headers=self.admin)
self.assertEqual(200, status)
for row in body["registrations"]:
if row["uuid"] == app_uuid:
return row
return None
def _audit(self):
with open(self.env["BROKER_AUDIT_LOG_PATH"], "r", encoding="utf-8") as handle:
return [json.loads(raw) for raw in handle if raw.strip()]
def test_list_empty_then_add_then_list(self):
status, body = self._request("GET", BASE_PATH, headers=self.admin)
self.assertEqual(200, status)
self.assertEqual({"registrations": []}, body)
app_uuid = str(uuidlib.uuid4())
status, body = self._add(app_uuid, "proxmox-tool")
self.assertEqual(201, status)
self.assertEqual({"uuid": app_uuid, "status": "active"}, body)
row = self._row(app_uuid)
self.assertEqual("proxmox-tool", row["name"])
self.assertEqual("active", row["status"])
self.assertTrue(row["created_at"])
self.assertIsNone(row["revoked_at"])
def test_add_without_name_stores_null_name(self):
app_uuid = str(uuidlib.uuid4())
status, _ = self._add(app_uuid)
self.assertEqual(201, status)
self.assertIsNone(self._row(app_uuid)["name"])
def test_add_duplicate_active_or_revoked_is_409(self):
app_uuid = str(uuidlib.uuid4())
self._add(app_uuid)
status, body = self._add(app_uuid)
self.assertEqual(409, status)
self.assertEqual(409, body["error"])
self.assertTrue(body["message"])
status, _ = self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
self.assertEqual(200, status)
status, body = self._add(app_uuid)
self.assertEqual(409, status)
def test_add_invalid_or_noncanonical_uuid_is_400(self):
canonical = str(uuidlib.uuid4())
for bad in ("not-a-uuid", "", "{" + canonical.upper() + "}", canonical.upper(),
canonical.replace("-", ""), "urn:uuid:" + canonical):
status, body = self._add(bad)
self.assertEqual(400, status, bad)
self.assertEqual(400, body["error"])
status, _ = self._request("POST", BASE_PATH, {}, self.admin)
self.assertEqual(400, status)
status, _ = self._request("POST", BASE_PATH, {"uuid": 12345}, self.admin)
self.assertEqual(400, status)
status, _ = self._request("POST", BASE_PATH, {"name": "no-uuid"}, self.admin)
self.assertEqual(400, status)
def test_revoke_revoke_again_reinstate_lifecycle(self):
app_uuid = str(uuidlib.uuid4())
self._add(app_uuid)
status, body = self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
self.assertEqual(200, status)
self.assertEqual("revoked", body["status"])
revoked_at = self._row(app_uuid)["revoked_at"]
self.assertIsNotNone(revoked_at)
self.assertEqual("revoked", self._row(app_uuid)["status"])
status, body = self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
self.assertEqual(200, status)
self.assertEqual(revoked_at, self._row(app_uuid)["revoked_at"])
status, body = self._request(
"POST", BASE_PATH + "/" + app_uuid + "/reinstate", headers=self.admin
)
self.assertEqual(200, status)
self.assertEqual("active", body["status"])
row = self._row(app_uuid)
self.assertEqual("active", row["status"])
self.assertIsNone(row["revoked_at"])
def test_revoke_missing_is_404(self):
status, body = self._request(
"DELETE", BASE_PATH + "/" + str(uuidlib.uuid4()), headers=self.admin
)
self.assertEqual(404, status)
self.assertEqual(404, body["error"])
def test_reinstate_missing_is_404(self):
status, body = self._request(
"POST", BASE_PATH + "/" + str(uuidlib.uuid4()) + "/reinstate", headers=self.admin
)
self.assertEqual(404, status)
self.assertEqual(404, body["error"])
def test_admin_actions_audited_with_null_identity(self):
app_uuid = str(uuidlib.uuid4())
self._add(app_uuid)
self._request("DELETE", BASE_PATH + "/" + app_uuid, headers=self.admin)
self._request("POST", BASE_PATH + "/" + app_uuid + "/reinstate", headers=self.admin)
entries = self._audit()
self.assertEqual(["add_app", "remove_app", "add_app"], [e["action"] for e in entries])
for entry in entries:
self.assertEqual("ok", entry["result"])
self.assertIsNone(entry["user_id"])
self.assertIsNone(entry["app_registration_uuid"])
self.assertIsNone(entry["agent_id"])
self.assertEqual("/api/broker/app_registrations/" + app_uuid, entry["path"])
def test_unauthenticated_requests_are_401(self):
checks = (
("GET", BASE_PATH, None),
("POST", BASE_PATH, {"uuid": str(uuidlib.uuid4())}),
("DELETE", BASE_PATH + "/" + str(uuidlib.uuid4()), None),
("POST", BASE_PATH + "/" + str(uuidlib.uuid4()) + "/reinstate", None),
)
for method, path, payload in checks:
status, body = self._request(method, path, payload)
self.assertEqual(401, status, method + " " + path)
self.assertEqual(401, body["error"])
if __name__ == "__main__":
unittest.main()