from __future__ import annotations import copy import hashlib import importlib.util import json from pathlib import Path import pytest ROOT = Path(__file__).resolve().parents[2] MODULE_PATH = ROOT / "ops" / "gitea-deploy-broker" / "registered_legacy_adoption.py" REPOSITORY = "jens/quiz-monitor" REQUEST_ID = "6f9619ff-8b86-4011-b42d-00c04fc964ff" TARGET_SHA = "a" * 40 KEY = b"k" * 32 def _load_module(): spec = importlib.util.spec_from_file_location("registered_legacy_adoption_test", MODULE_PATH) assert spec and spec.loader module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module @pytest.fixture def module(): return _load_module() @pytest.fixture def project(): return { "slug": "quiz-monitor", "branch": "main", "compose_project": "quiz-monitor", "compose_files": ["docker-compose.yml"], "containers": ["quiz-monitor-quiz-server-1"], "source_dir": "/srv/example/appdata/quiz-monitor/source", "verify_oci_revision": True, } @pytest.fixture def baseline_inputs(): state_bytes = b'{"schema_version":3,"live_revision":"0c9c1747","live_status":"unknown"}\n' image_id = "sha256:" + "a" * 64 observation = { "sourceFingerprint": "1" * 64, "runtimeFingerprint": "2" * 64, "imageIDs": {"quiz-monitor-quiz-server-1": image_id}, "runtimeConfiguration": { "environment": {"OPENRGB_HOST": "198.19.1.2", "OPENRGB_TOKEN": "private-test-value"}, "composeModel": {"services": {"quiz-server": {"ports": ["1205:8000"]}}}, }, "artifacts": { "containers": { "quiz-monitor-quiz-server-1": { "image_id": image_id, "container_id": "b" * 64, "image_ref": "quiz-monitor:latest", "service": "quiz-server", "running": True, } } }, } return state_bytes, observation class _Runtime: def __init__(self, observation): self.observation = observation self.restored = [] self.snapshots = [] def observe(self, repository, target, *, receipt_key): assert repository == REPOSITORY assert target["slug"] == "quiz-monitor" assert receipt_key == KEY return copy.deepcopy(self.observation) def snapshot(self, repository, target, observation, request_id, *, receipt_key): value = { "revision": "unknown", "legacy_provenance": True, "containers": [ { "name": "quiz-monitor-quiz-server-1", "image_id": observation["imageIDs"]["quiz-monitor-quiz-server-1"], "image_ref": "quiz-monitor:latest", "rollback_ref": "gitea-deploy-rollback/quiz-monitor-0:legacy-6f9619ff", "was_running": True, } ], } self.snapshots.append(value) return copy.deepcopy(value) def restore(self, repository, target, snapshot, current_revision, *, receipt_key): self.restored.append((repository, target["slug"], snapshot, current_revision)) return copy.deepcopy(self.observation) @pytest.fixture def api(): def api_json(path, *, query): if path == f"/repos/{REPOSITORY}/branches/main": return {"protected": True, "commit": {"id": TARGET_SHA}} if path == f"/repos/{REPOSITORY}/actions/runs": assert query == { "event": "push", "branch": "main", "head_sha": TARGET_SHA, "page": "1", "limit": "100", } return {"total_count": 1, "workflow_runs": [{ "id": 7, "run_number": 19, "run_attempt": 1, "event": "push", "head_sha": TARGET_SHA, "head_branch": "main", "path": "managed-validation.yml@refs/heads/main", "display_title": "Managed validation / full (push)", "created_at": "2026-10-07T12:00:00Z", "status": "completed", "conclusion": "success", "repository": {"full_name": REPOSITORY}, "head_repository": {"full_name": REPOSITORY}, "pull_requests": [], }]} if path == f"/repos/{REPOSITORY}/actions/runs/7/jobs": assert query == {"page": "1", "limit": "100"} return {"total_count": 1, "jobs": [{ "id": 9, "name": "full", "run_id": 7, "run_attempt": 1, "head_sha": TARGET_SHA, "head_branch": "main", "status": "completed", "conclusion": "success", }]} raise AssertionError(f"unexpected Gitea read: {path}") return api_json def _read_state(state_bytes): state = json.loads(state_bytes.decode("utf-8")) if state_bytes else {} return lambda _repository, _project: (state, state_bytes) def _baseline(module, project, state_bytes, observation, tmp_path): (tmp_path / "quiz-monitor.json").write_bytes(state_bytes) return module.baseline( {"repository": REPOSITORY, "requestId": REQUEST_ID}, registry={REPOSITORY: project}, read_state=_read_state(state_bytes), runtime=_Runtime(observation), receipt_key=KEY, state_root=tmp_path, ) def test_registered_target_policy_accepts_bounded_main_compose_and_rejects_special_targets( module, project ): assert module.validate_registered_legacy_target(REPOSITORY, project) == "quiz-monitor" for repository, mutate in [ ("jens/unraid-appops-gateway", lambda candidate: None), ("jens/workmesh", lambda candidate: None), (REPOSITORY, lambda candidate: candidate.update(managed_plan_version="itworx.deployment/v1")), (REPOSITORY, lambda candidate: candidate.update(deploy_script="scripts/deploy.sh")), (REPOSITORY, lambda candidate: candidate.update(containers=[f"quiz-monitor-svc-{n}" for n in range(9)])), (REPOSITORY, lambda candidate: candidate.update(compose_files=["../other/docker-compose.yml"])), ]: candidate = copy.deepcopy(project) mutate(candidate) with pytest.raises(module.RegisteredLegacyAdoptionError): module.validate_registered_legacy_target(repository, candidate) def test_rendered_compose_must_match_observed_environment_mounts_and_published_ports(module): model = {"services": {"quiz-server": { "environment": {"OPENRGB_HOST": "198.19.1.2"}, "volumes": [{"type": "bind", "source": "/srv/example/appdata/quiz-monitor/config", "target": "/config", "read_only": False}], "ports": [{"target": 8000, "published": "1205", "protocol": "tcp", "host_ip": "0.0.0.0"}], }}, "networks": {"default": {"name": "quiz-monitor_default"}}} container = {"quiz-monitor-quiz-server-1": { "Config": {"Env": ["OPENRGB_HOST=198.19.1.2"]}, "Mounts": [{"Type": "bind", "Source": "/srv/example/appdata/quiz-monitor/config", "Destination": "/config", "RW": True}], "HostConfig": {"NetworkMode": "quiz-monitor_default"}, "NetworkSettings": {"Networks": {"quiz-monitor_default": {}}, "Ports": {"8000/tcp": [{"HostIp": "0.0.0.0", "HostPort": "1205"}]}}, }} module.validate_compose_runtime(model, container) wrong = copy.deepcopy(container) wrong["quiz-monitor-quiz-server-1"]["NetworkSettings"]["Ports"]["8000/tcp"][0]["HostPort"] = "1206" with pytest.raises(module.RegisteredLegacyAdoptionError): module.validate_compose_runtime(model, wrong) def test_rendered_compose_rejects_changed_command_security_and_named_volume_identity(module): model = {"services": {"quiz-server": { "image": "quiz-monitor:latest", "command": ["python", "server.py"], "security_opt": ["no-new-privileges:true"], "volumes": [{"type": "volume", "source": "quiz-config", "target": "/config"}], }}, "volumes": {"quiz-config": {"name": "quiz-monitor-config"}}, "networks": {"default": {"name": "quiz-monitor_default"}}} original = {"quiz-monitor-quiz-server-1": { "Config": {"Cmd": ["python", "server.py"], "Labels": {"com.docker.compose.service": "quiz-server"}}, "HostConfig": {"SecurityOpt": ["no-new-privileges:true"], "NetworkMode": "quiz-monitor_default"}, "Mounts": [{"Type": "volume", "Name": "quiz-monitor-config", "Destination": "/config", "RW": True}], "NetworkSettings": {"Networks": {"quiz-monitor_default": {}}, "Ports": {}}, }} module.validate_compose_runtime(model, original, compose_project="quiz-monitor") changed = copy.deepcopy(original) changed["quiz-monitor-quiz-server-1"]["Config"]["Cmd"] = ["sh", "-c", "unexpected"] with pytest.raises(module.RegisteredLegacyAdoptionError): module.validate_compose_runtime(model, changed, compose_project="quiz-monitor") changed = copy.deepcopy(original) changed["quiz-monitor-quiz-server-1"]["HostConfig"]["SecurityOpt"] = [] with pytest.raises(module.RegisteredLegacyAdoptionError): module.validate_compose_runtime(model, changed, compose_project="quiz-monitor") changed = copy.deepcopy(original) changed["quiz-monitor-quiz-server-1"]["Mounts"][0]["Name"] = "unexpected-volume" with pytest.raises(module.RegisteredLegacyAdoptionError): module.validate_compose_runtime(model, changed, compose_project="quiz-monitor") def test_baseline_binds_exact_runtime_state_without_revealing_stale_sha_or_secrets( module, project, baseline_inputs, tmp_path ): state_bytes, observation = baseline_inputs result = _baseline(module, project, state_bytes, observation, tmp_path) assert result["previousRevision"] == "unknown" assert result["expectedStateSha256"] == hashlib.sha256(state_bytes).hexdigest() assert result["runtimeFingerprint"] == "2" * 64 assert result["imageIDs"] == observation["imageIDs"] assert "0c9c1747" not in json.dumps(result) assert "private-test-value" not in json.dumps(result) root = tmp_path / "registered-legacy-adoption" / "quiz-monitor" assert (root / f"{REQUEST_ID}.baseline.json").is_file() private = root / f"{REQUEST_ID}.private.json" assert private.is_file() assert "private-test-value" not in (root / f"{REQUEST_ID}.baseline.json").read_text() def test_preflight_requires_latest_exact_main_push_and_registered_full_job( module, project, baseline_inputs, api, tmp_path ): state_bytes, observation = baseline_inputs baseline = _baseline(module, project, state_bytes, observation, tmp_path) result = module.preflight( {"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"]}, registry={REPOSITORY: project}, read_state=_read_state(state_bytes), runtime=_Runtime(observation), api_json=api, receipt_key=KEY, state_root=tmp_path, ) assert result["status"] == "preflight-ok" assert result["revision"] == TARGET_SHA assert result["validationEvidenceSha256"] assert result["preflightSha256"] @pytest.mark.parametrize("invalid", ["pr_event", "wrong_sha", "failed_latest", "missing_full_job", "unprotected"]) def test_preflight_rejects_noncanonical_or_failed_latest_validation( module, project, baseline_inputs, api, tmp_path, invalid ): state_bytes, observation = baseline_inputs baseline = _baseline(module, project, state_bytes, observation, tmp_path) def bad_api(path, *, query): value = api(path, query=query) if path.endswith("/actions/runs"): run = value["workflow_runs"][0] if invalid == "pr_event": run["event"] = "pull_request" elif invalid == "wrong_sha": run["head_sha"] = "b" * 40 elif invalid == "failed_latest": run["run_attempt"] = 2 run["conclusion"] = "failure" elif path.endswith("/branches/main") and invalid == "unprotected": value["protected"] = False elif path.endswith("/jobs") and invalid == "missing_full_job": value["jobs"][0]["name"] = "backend" return value with pytest.raises(module.RegisteredLegacyAdoptionError): module.preflight( {"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"]}, registry={REPOSITORY: project}, read_state=_read_state(state_bytes), runtime=_Runtime(observation), api_json=bad_api, receipt_key=KEY, state_root=tmp_path, ) def test_adoption_rechecks_gate_and_cas_then_saves_unknown_rollback_proof( module, project, baseline_inputs, api, tmp_path ): state_bytes, observation = baseline_inputs (tmp_path / "quiz-monitor.json").write_bytes(state_bytes) runtime = _Runtime(observation) registry = {REPOSITORY: project} read_state = _read_state(state_bytes) baseline = module.baseline( {"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry, read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=tmp_path, ) preflight = module.preflight( {"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"]}, registry=registry, read_state=read_state, runtime=runtime, api_json=api, receipt_key=KEY, state_root=tmp_path, ) # Adoption independently rereads main and validation rather than trusting the saved preflight. calls = [] def rechecked_api(path, *, query): calls.append(path) return api(path, query=query) result = module.adopt( {"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"], "preflightSha256": preflight["preflightSha256"]}, registry=registry, read_state=read_state, runtime=runtime, api_json=rechecked_api, receipt_key=KEY, state_root=tmp_path, ) assert result["status"] == "adopted" assert result["previousRevision"] == "unknown" assert any(path.endswith("/actions/runs") for path in calls) receipt = tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.receipt.json" assert receipt.is_file() assert "private-test-value" not in receipt.read_text() snapshot = module.capture_deployment_snapshot( REPOSITORY, project, json.loads(state_bytes), request_id=REQUEST_ID, target_revision=TARGET_SHA, receipt_key=KEY, state_root=tmp_path, runtime=runtime, ) assert snapshot["revision"] == "unknown" assert snapshot["legacy_provenance"] is True assert snapshot["containers"][0]["image_id"] == observation["imageIDs"]["quiz-monitor-quiz-server-1"] assert snapshot["registered_legacy"]["snapshotSha256"] assert module.status( {"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry, read_state=read_state, receipt_key=KEY, state_root=tmp_path, )["status"] == "adopted" def test_adoption_rejects_a_changed_controller_state_before_snapshot( module, project, baseline_inputs, api, tmp_path ): state_bytes, observation = baseline_inputs current_bytes = [state_bytes] def read_state(_repository, _project): return json.loads(current_bytes[0].decode("utf-8")), current_bytes[0] runtime = _Runtime(observation) registry = {REPOSITORY: project} baseline = module.baseline( {"repository": REPOSITORY, "requestId": REQUEST_ID}, registry=registry, read_state=read_state, runtime=runtime, receipt_key=KEY, state_root=tmp_path, ) preflight = module.preflight( {"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"]}, registry=registry, read_state=read_state, runtime=runtime, api_json=api, receipt_key=KEY, state_root=tmp_path, ) current_bytes[0] += b" " with pytest.raises(module.RegisteredLegacyAdoptionError): module.adopt( {"repository": REPOSITORY, "revision": TARGET_SHA, "requestId": REQUEST_ID, "expectedStateSha256": baseline["expectedStateSha256"], "preflightSha256": preflight["preflightSha256"]}, registry=registry, read_state=read_state, runtime=runtime, api_json=api, receipt_key=KEY, state_root=tmp_path, ) assert not (tmp_path / "registered-legacy-adoption" / "quiz-monitor" / f"{REQUEST_ID}.receipt.json").exists() def test_status_is_nonblocking_and_worker_failure_is_durable_and_terminal_safe( module, project, baseline_inputs, tmp_path ): state_bytes, observation = baseline_inputs _baseline(module, project, state_bytes, observation, tmp_path) pending = module.record_worker_failure( REPOSITORY, REQUEST_ID, "worker_start_failed", receipt_key=KEY, state_root=tmp_path, ) assert pending["status"] == "recovery-required" assert module.record_worker_failure( REPOSITORY, REQUEST_ID, "second_failure", receipt_key=KEY, state_root=tmp_path, )["status"] == "recovery-required" assert module.status( {"repository": REPOSITORY, "requestId": REQUEST_ID}, registry={REPOSITORY: project}, read_state=_read_state(state_bytes), receipt_key=KEY, state_root=tmp_path, )["status"] == "recovery-required"