from __future__ import annotations import sys from types import SimpleNamespace import pytest from test_hub_component_transport import call from test_hub_component_transport import controller as _controller_fixture REPO = "jens/quiz-monitor" REQUEST = "11111111-1111-4111-8111-111111111111" PREFIX = "/v1/appops/registered-legacy/" @pytest.fixture def controller(monkeypatch): return _controller_fixture.__wrapped__(monkeypatch) @pytest.fixture def adoption(monkeypatch): observations = [] def status(payload, **_kwargs): return {"status": "adopted", "repository": payload["repository"], "requestId": payload["requestId"], "revision": "a" * 40, "expectedStateSha256": "b" * 64} fake = SimpleNamespace(RegisteredLegacyAdoptionError=ValueError, read_registered_state=lambda *_a: ({}, None), status=status, baseline=lambda payload, **_k: {"status": "ready", **payload}, accept_rollback=lambda payload, **_k: {"status": "rollback_pending", **payload}, record_worker_failure=lambda *args, **_k: observations.append(args)) monkeypatch.setitem(sys.modules, "registered_legacy_adoption", fake) return fake, observations def invoke(module, stage, payload): return call(module, PREFIX + stage, payload, "_handle_registered_legacy") def rollback_payload(): return {"repository": REPO, "requestId": REQUEST, "revision": "a" * 40, "expectedStateSha256": "b" * 64} def test_generic_authentication_rejects_before_helper_loading(controller, monkeypatch): module, _ = controller monkeypatch.setattr(module, "_self_upgrade_signature_is_valid", lambda *_a, **_k: False) assert invoke(module, "adopt", {"image": "attacker/image"})[0] == 403 @pytest.mark.parametrize("change", [{"repository": "Jens/quiz-monitor"}, {"repository": "../escape"}, {"requestId": "bad"}, {"requestId": 123}]) def test_generic_envelope_rejects_invalid_identity_before_helpers(controller, adoption, change): module, _ = controller assert invoke(module, "baseline", {"repository": REPO, "requestId": REQUEST, **change})[0] == 400 def test_delivery_requires_adopted_exact_target_and_preserves_original_revalidation(controller, adoption, monkeypatch): module, _ = controller called = [] monkeypatch.setattr(module, "_request_terminal_revalidation", lambda *args, **kwargs: called.append((args, kwargs)) or {"status": "awaiting_validation"}) payload = {"repository": REPO, "requestId": REQUEST, "revision": "a" * 40} assert invoke(module, "delivery", {**payload, "revision": "c" * 40})[1]["reason"] == "registered_delivery_adoption_required" assert invoke(module, "delivery", {**payload, "image": "caller/image"})[1]["reason"] == "registered_delivery_contract_invalid" assert not called assert invoke(module, "delivery", payload)[1]["status"] == "awaiting_validation" assert called == [((REPO, "a" * 40), {"registered_legacy_request_id": REQUEST})] def test_only_internal_adopted_retry_binds_request_to_deployer_environment(controller, monkeypatch): module, _ = controller called = [] monkeypatch.setenv("APPOPS_REGISTERED_LEGACY_REQUEST_ID", "inherited-untrusted-value") monkeypatch.setattr(module.subprocess, "run", lambda args, **kwargs: called.append((args, kwargs)) or SimpleNamespace(returncode=0)) module._run_registered_deploy(REPO, "a" * 40) assert "APPOPS_REGISTERED_LEGACY_REQUEST_ID" not in called[-1][1]["env"] module._run_registered_deploy(REPO, "a" * 40, registered_legacy_request_id=REQUEST) assert called[-1][1]["env"]["APPOPS_REGISTERED_LEGACY_REQUEST_ID"] == REQUEST assert REQUEST not in called[-1][0] with pytest.raises(ValueError): module._run_registered_deploy(REPO, "a" * 40, registered_legacy_request_id="bad") with pytest.raises(ValueError): module._run_registered_deploy(module.REGISTERED_AUTO_APPLY_REPOSITORY, "a" * 40, registered_legacy_request_id=REQUEST) def test_rollback_queue_is_bounded_idempotent_and_rejects_changed_binding(controller, adoption, monkeypatch): module, _ = controller starts = [] monkeypatch.setattr(module.threading, "Thread", lambda **kwargs: SimpleNamespace(start=lambda: starts.append(kwargs))) payload = rollback_payload() assert invoke(module, "rollback", payload)[0] == 202 assert invoke(module, "rollback", payload)[0] == 200 assert len(starts) == 1 assert invoke(module, "rollback", {**payload, "expectedStateSha256": "c" * 64})[1]["reason"] == "registered_rollback_binding_changed" assert invoke(module, "rollback", {**payload, "requestId": "22222222-2222-4222-8222-222222222222"})[1]["reason"] == "registered_rollback_capacity_busy" def test_rollback_start_failure_is_recorded_and_capacity_released(controller, adoption, monkeypatch): module, _ = controller _, observations = adoption def fail_start(**_kwargs): raise RuntimeError("thread start unavailable") monkeypatch.setattr(module.threading, "Thread", fail_start) assert invoke(module, "rollback", rollback_payload())[1]["reason"] == "registered_rollback_worker_start_failed" assert observations == [(REPO, REQUEST, "registered_rollback_worker_start_failed")] assert not module._registered_legacy_worker_requests def test_worker_never_discloses_raw_failure_and_releases_capacity(controller, adoption): module, _ = controller fake, observations = adoption def fail(*_args, **_kwargs): raise RuntimeError("private environment credential") fake.rollback = fail module._registered_legacy_worker_requests.add(REPO + "/" + REQUEST) module._run_registered_legacy_rollback(rollback_payload(), b"x" * 64) assert observations == [(REPO, REQUEST, "registered_rollback_worker_failed")] assert not module._registered_legacy_worker_requests def test_failed_delivery_retry_requires_explicit_adoption_and_keeps_original_record(controller, monkeypatch, tmp_path): module, _ = controller revision = "a" * 40 semantic = module._webhook_semantic_digest(6, "refs/heads/main", revision) original = {"schema": module._WEBHOOK_SCHEMA, "semantic_sha256": semantic, "gitea_repository_id": 6, "ref": "refs/heads/main", "repository": REPO, "revision": revision, "body_sha256": "b" * 64, "validation": {}, "status": "failed", "reason": "deployment_failed"} written = [] monkeypatch.setattr(module, "WEBHOOK_INBOX_ROOT", tmp_path) monkeypatch.setattr(module, "_secure_inbox_root", lambda: None) monkeypatch.setattr(module, "_webhook_policy", lambda *_a: (6, "main", "fixed-secret-file")) monkeypatch.setattr(module, "_validation_policy", lambda *_a: {}) monkeypatch.setattr(module, "_validation_snapshot_matches", lambda *_a: True) monkeypatch.setattr(module, "_validation_snapshot", lambda *_a: {"required": True}) monkeypatch.setattr(module, "_read_inbox_record", lambda _p: original) monkeypatch.setattr(module, "_is_current_branch_head", lambda *_a: True) monkeypatch.setattr(module, "_write_new_inbox_record", lambda path, record: written.append((path, record))) assert module._enqueue_terminal_revalidation(REPO, revision, {})[0] == "not_revalidatable" assert module._enqueue_terminal_revalidation(REPO, revision, {}, registered_legacy_request_id="bad")[0] == "invalid" assert not written assert module._enqueue_terminal_revalidation(REPO, revision, {}, registered_legacy_request_id=REQUEST)[0] == "awaiting_validation" path, record = written[0] assert path.name == semantic + "." + REQUEST + ".revalidation.json" assert record["registered_legacy_request_id"] == REQUEST assert record["validation"] == {"required": True} assert original["status"] == "failed" and original["reason"] == "deployment_failed" def test_webhook_cannot_inject_registered_adoption_binding(controller, monkeypatch, tmp_path): module, _ = controller monkeypatch.setattr(module, "_read_inbox_record", lambda _p: { "schema": module._WEBHOOK_SCHEMA, "status": "validation_passed", "registered_legacy_request_id": REQUEST, }) monkeypatch.setattr(module, "_load_projects", lambda: pytest.fail("injected webhook must be rejected before processing")) module._process_webhook_record(tmp_path / "a.json")