from __future__ import annotations import hashlib import io import json import subprocess import sys from email.message import Message from types import SimpleNamespace import pytest from test_deploy_broker import BROKER_ROOT, _load_module @pytest.fixture def controller(monkeypatch): module = _load_module("component_transport_test", BROKER_ROOT / "status_server.py") monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: True) monkeypatch.setattr(module, "_self_upgrade_secret", lambda: b"transport-test-key-" + b"x" * 48) projects = {module.REGISTERED_AUTO_APPLY_REPOSITORY: {"branch": "main", "validation": {"required": True}}, "other/unchanged": {"branch": "main"}} monkeypatch.setattr(module, "_load_projects", lambda: projects) monkeypatch.setattr(module, "_write_projects", lambda updated: (projects.clear(), projects.update(updated))) monkeypatch.setattr(module, "_validation_policy", lambda *_a: {"required": True}) return module, projects def call(module, path, payload, method="_handle_registered_auto_apply"): handler = module.Handler.__new__(module.Handler) body = json.dumps(payload).encode() handler.path = 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, data: responses.append((status, json.loads(data))) getattr(handler, method)() return responses[0] def intent(module, projects): project = projects[module.REGISTERED_AUTO_APPLY_REPOSITORY] return {"repository": module.REGISTERED_AUTO_APPLY_REPOSITORY, "held": True, "expectedRegistrySha256": hashlib.sha256(json.dumps(project, sort_keys=True, separators=(",", ":")).encode()).hexdigest(), "reason": "Limit rollout to the validated Gitea component", "requestId": "11111111-1111-4111-8111-111111111111"} def test_hold_is_cas_bound_and_retains_registered_policy(controller): module, projects = controller payload = intent(module, projects) original = dict(projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]) code, report = call(module, module.REGISTERED_AUTO_APPLY_PATH, payload) assert code == 200 and report["status"] == "held" and report["autoApply"] is False updated = projects[module.REGISTERED_AUTO_APPLY_REPOSITORY] assert updated["branch"] == original["branch"] assert updated["validation"] == original["validation"] assert projects["other/unchanged"] == {"branch": "main"} assert payload["reason"] not in json.dumps(updated) assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[1]["reason"] == "registered_target_policy_changed" code, preflight = call(module, module.REGISTERED_AUTO_APPLY_PATH + "/preflight", {"repository": payload["repository"]}) assert code == 200 and preflight["autoApply"] is False payload.update(held=False, expectedRegistrySha256=preflight["registrySha256"]) assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[1]["status"] == "released" @pytest.mark.parametrize("change", [{"repository": "other/unchanged"}, {"held": "true"}, {"image": "caller/image"}, {"requestId": "bad"}, {"expectedRegistrySha256": "0" * 64}]) def test_hold_rejects_unknown_target_input_and_stale_fingerprint(controller, change): module, projects = controller before = json.dumps(projects, sort_keys=True) code, _ = call(module, module.REGISTERED_AUTO_APPLY_PATH, {**intent(module, projects), **change}) assert code in {400, 409} assert json.dumps(projects, sort_keys=True) == before def test_hold_authentication_and_inflight_deployment_remain_fail_closed(controller, monkeypatch): module, projects = controller payload = intent(module, projects) monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: False) assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[0] == 403 monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: True) module._registered_auto_apply_lock.acquire() try: assert call(module, module.REGISTERED_AUTO_APPLY_PATH, payload)[1]["reason"] == "registered_target_deployment_active" finally: module._registered_auto_apply_lock.release() def test_execution_rechecks_hold_under_same_lock(controller, monkeypatch): module, projects = controller invoked = [] def execute(command, **_kwargs): assert module._registered_auto_apply_lock.acquire(blocking=False) is False invoked.append(command) return subprocess.CompletedProcess(command, 0) monkeypatch.setattr(module.subprocess, "run", execute) projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]["auto_apply"] = False assert module._run_registered_deploy(module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40).returncode == 4 assert invoked == [] projects[module.REGISTERED_AUTO_APPLY_REPOSITORY]["auto_apply"] = True assert module._run_registered_deploy(module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40).returncode == 0 assert invoked == [["python3", str(module.ROOT / "deploy.py"), "deploy", module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40]] def test_component_authentication_blocks_before_reading_or_mutating(controller, monkeypatch): module, _ = controller monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: False) assert call(module, "/v1/appops/hub-components/apply", {"image": "caller/image"}, "_handle_hub_component")[0] == 403 def test_component_worker_serializes_hold_and_records_sanitized_failure(controller, monkeypatch): module, _ = controller failures = [] plan_id = "a" * 64 def apply(*_args, **_kwargs): assert module._registered_auto_apply_lock.acquire(blocking=False) is False raise RuntimeError("secret runtime configuration must never escape") fake = SimpleNamespace(apply=apply, PromotionRejected=ValueError, record_worker_failure=lambda *args, **_kw: failures.append(args)) monkeypatch.setitem(sys.modules, "immutable_service_promotion", fake) module._component_worker_plans.add(plan_id) module._run_component_worker({"planId": plan_id}, b"x" * 64) assert failures == [(plan_id, "component_worker_failed")] assert plan_id not in module._component_worker_plans assert module._registered_auto_apply_lock.acquire(blocking=False) module._registered_auto_apply_lock.release() def test_component_queue_is_idempotent_and_failed_start_is_durable(controller, monkeypatch): module, _ = controller failures, starts = [], [] plan_id = "a" * 64 fake = SimpleNamespace(accept_apply=lambda *_a, **_k: {"status": "queued", "planId": plan_id}, PromotionRejected=ValueError, record_worker_failure=lambda *args, **_kw: failures.append(args)) monkeypatch.setitem(sys.modules, "immutable_service_promotion", fake) monkeypatch.setattr(module.threading, "Thread", lambda **kwargs: SimpleNamespace(start=lambda: starts.append(kwargs))) payload = {"planId": plan_id} assert call(module, "/v1/appops/hub-components/apply", payload, "_handle_hub_component")[0] == 202 assert call(module, "/v1/appops/hub-components/apply", payload, "_handle_hub_component")[0] == 202 assert len(starts) == 1 module._component_worker_plans.clear() def fail_start(**_kwargs): raise RuntimeError("thread unavailable") monkeypatch.setattr(module.threading, "Thread", fail_start) assert call(module, "/v1/appops/hub-components/apply", payload, "_handle_hub_component")[1]["reason"] == "component_worker_start_failed" assert failures == [(plan_id, "component_worker_start_failed")] assert not module._component_worker_plans @pytest.mark.parametrize("validated", [False, True]) def test_hold_retains_push_validation_and_candidate_preparation_without_deploy(controller, monkeypatch, tmp_path, validated): module, projects = controller repository = module.REGISTERED_AUTO_APPLY_REPOSITORY projects[repository]["auto_apply"] = False revision = "a" * 40 semantic = module._webhook_semantic_digest(34, "refs/heads/main", revision) marker = tmp_path / (semantic + ".json") record = {"schema": module._WEBHOOK_SCHEMA, "repository": repository, "gitea_repository_id": 34, "ref": "refs/heads/main", "revision": revision, "semantic_sha256": semantic, "body_sha256": "b" * 64, "status": "validation_passed" if validated else "awaiting_validation"} monkeypatch.setattr(module, "_read_inbox_record", lambda _path: record) monkeypatch.setattr(module, "_webhook_policy", lambda *_a: (34, "main")) monkeypatch.setattr(module, "_record_policy_for_processing", lambda *_a: True) monkeypatch.setattr(module, "_process_validation", lambda *_a: False) monkeypatch.setattr(module, "_atomic_inbox_update", lambda *_a: None) monkeypatch.setattr(module, "_supersede_older_deliveries", lambda *_a: None) prepared = [] monkeypatch.setattr(module, "_prepare_hub_candidate_delivery", lambda *_a: prepared.append(revision) or True) monkeypatch.setattr(module, "_run_registered_deploy", lambda *_a: pytest.fail("held target must never deploy")) module._process_webhook_record(marker) assert prepared == ([revision] if validated else []) assert record["status"] == ("validation_passed" if validated else "awaiting_validation") if validated: assert record["reason"] == "registered_target_auto_apply_held"