Files
Unraid-AppOps-Gateway-Public/backend/tests/test_registered_legacy_http.py
T

155 lines
8.2 KiB
Python

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")