"""Runtime readiness is independent of storage for enrolling a new target.""" from __future__ import annotations import http.client import io import json import threading from email.message import Message import pytest from test_deploy_broker import BROKER_ROOT, _load_module @pytest.fixture def controller(monkeypatch, tmp_path): module = _load_module("appops_runtime_capabilities_test", BROKER_ROOT / "status_server.py") repository_root = tmp_path / "repos" repository_root.mkdir() secret_path = tmp_path / "bootstrap" secret = b"runtime-capabilities-test-" + b"x" * 48 secret_path.write_bytes(secret) secret_path.chmod(0o600) monkeypatch.setattr(module, "APPOPS_ENROLLMENT_BOOTSTRAP_SECRET_FILE", secret_path) monkeypatch.setattr(module, "APPOPS_NONCE_ROOT", tmp_path / "nonces") monkeypatch.setattr(module, "REPOSITORY_ROOT", repository_root) monkeypatch.setattr(module, "_controller_release", lambda: {"revision": "a" * 40}) monkeypatch.setattr(module, "_projects_registry_is_safe", lambda: True) monkeypatch.setattr(module, "_load_projects", lambda: {"owner/existing": {}}) monkeypatch.setattr(module, "_refresh_appdata_root_selection", lambda: None) monkeypatch.setattr(module, "trusted_appdata_roots_readiness", lambda: [ {"root": "/srv/example/appdata", "status": "unavailable", "reason": "unsafe_permissions"}, ]) monkeypatch.setattr(module, "managed_storage_readiness", lambda: {"status": "needs_initialization"}) server = module.BoundedThreadingHTTPServer(("127.0.0.1", 0), module.Handler) serving = threading.Thread(target=server.serve_forever, daemon=True) serving.start() counter = 0 def request(*, authenticated=True, replay_headers=None): nonlocal counter counter += 1 body = b"{}" path = "/v1/appops/enrollments/capabilities" timestamp = str(int(module.time.time())) nonce = f"{counter:032x}" headers = replay_headers or { "Content-Type": "application/json", "X-ITWorx-Deployment-Timestamp": timestamp, "X-ITWorx-Deployment-Nonce": nonce, "X-ITWorx-Deployment-Signature": module._request_signature( secret, method="POST", path=path, timestamp=timestamp, nonce=nonce, raw_body=body, ) if authenticated else "0" * 64, } client = http.client.HTTPConnection(*server.server_address, timeout=2) client.request("POST", path, body=body, headers=headers) response = client.getresponse() payload = json.loads(response.read()) client.close() assert secret.decode() not in json.dumps(payload) return response.status, payload, headers yield module, request server.shutdown() server.server_close() serving.join(timeout=2) def test_registered_runtime_ready_without_new_enrollment_storage(controller): _, request = controller status, payload, headers = request() assert status == 200 assert payload["status"] == "ready" assert payload["registry"] == {"status": "ready", "registeredProjectCount": 1} assert payload["enrollmentReadiness"] == { "status": "unavailable", "reason": "trusted_appdata_roots_unavailable", } for name in ("deploymentApply", "deploymentVerify", "rollback", "exactTargetPolicy", "hmacV2", "replayProtection"): assert payload["features"][name] is True for name in ("enrollment", "targetRegistration", "runtimeSecretSealing"): assert payload["features"][name] is False assert request(replay_headers=headers)[0] == 403 assert request(authenticated=False)[0] == 403 @pytest.mark.parametrize("unavailable", ["registry", "registry_content", "artifact", "repository"]) def test_runtime_prerequisites_remain_fail_closed(controller, monkeypatch, tmp_path, unavailable): module, request = controller if unavailable == "registry": monkeypatch.setattr(module, "_projects_registry_is_safe", lambda: False) elif unavailable == "registry_content": def invalid_registry(): raise ValueError("invalid registry") monkeypatch.setattr(module, "_load_projects", invalid_registry) elif unavailable == "artifact": monkeypatch.setattr(module, "_controller_release", lambda: None) else: monkeypatch.setattr(module, "REPOSITORY_ROOT", tmp_path / "missing") status, payload, _ = request() assert status == 503 assert payload["reason"] == "controller_runtime_prerequisite_unavailable" for name in ("deploymentApply", "deploymentVerify", "rollback", "exactTargetPolicy", "enrollment", "targetRegistration"): assert payload["features"][name] is False def test_enrollment_recovers_when_safe_root_becomes_ready(controller, monkeypatch): module, request = controller assert request()[1]["features"]["enrollment"] is False monkeypatch.setattr(module, "trusted_appdata_roots_readiness", lambda: [{"status": "ready"}]) status, payload, _ = request() assert status == 200 assert payload["enrollmentReadiness"] == {"status": "ready", "reason": ""} for name in ("enrollment", "targetRegistration", "runtimeSecretSealing"): assert payload["features"][name] is True @pytest.mark.parametrize("current,reason", [ ({"status": "blocked", "checks": [{"name": "required_push_validation", "passed": False}]}, "controller_preflight_failed_required_push_validation"), ({"status": "preflight_ok", "currentRevision": "b" * 40, "checks": []}, "current_revision_changed"), ]) def test_self_upgrade_reports_failed_gate_before_missing_current_revision(controller, monkeypatch, current, reason): module, _ = controller monkeypatch.setattr(module, "_self_upgrade_preflight", lambda _revision: dict(current)) monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_args, **_kwargs: True) intent = {"repository": module.SELF_UPGRADE_REPOSITORY, "revision": "c" * 40, "reason": "Validated infrastructure repair", "expectedCurrentRevision": "a" * 40} body = json.dumps(intent).encode() handler = module.Handler.__new__(module.Handler) handler.path = module.SELF_UPGRADE_PREFLIGHT_PATH handler.headers = Message() handler.headers["Content-Length"] = str(len(body)) handler.headers["Content-Type"] = "application/json" handler.rfile = io.BytesIO(body) handler._trusted_client = lambda: True responses = [] handler._send_json = lambda status, payload: responses.append((status, json.loads(payload))) handler._handle_self_upgrade() assert responses[0][0] == 409 assert responses[0][1]["reason"] == reason