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

414 lines
13 KiB
Python

from __future__ import annotations
import hashlib
import importlib.util
import io
import json
import sys
from dataclasses import replace
from pathlib import Path
from types import SimpleNamespace
import pytest
REPO_ROOT = Path(__file__).resolve().parents[2]
CLI_PATH = REPO_ROOT / "ops" / "gitea-deploy-broker" / "repopulse_adoption_cli.py"
def _load_module(name: str, path: Path):
spec = importlib.util.spec_from_file_location(name, path)
assert spec and spec.loader
module = importlib.util.module_from_spec(spec)
sys.modules[name] = module
spec.loader.exec_module(module)
return module
cli = _load_module("repopulse_adoption_cli_test", CLI_PATH)
def _sha(value: bytes | str) -> str:
raw = value.encode("ascii") if isinstance(value, str) else value
return hashlib.sha256(raw).hexdigest()
def _canonical_sha(value: object) -> str:
raw = json.dumps(
value,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
allow_nan=False,
).encode("ascii")
return hashlib.sha256(raw).hexdigest()
def _projection() -> dict[str, object]:
return {
"repository": "jens/repopulse",
"main_revision": "a" * 40,
"live_revision": "b" * 40,
"container_id_sha256": _sha("legacy-container"),
"container_image_sha256": _sha("legacy-image"),
"queue": {"queued": 0, "active": 0, "completed": 10, "failed": 0},
"database": {
"database_bytes": 12345,
"journal_bytes": 0,
"scan_count": 10,
"compacted_scan_count": 4,
},
"container_state": "running",
"container_health": "healthy",
}
def _payload() -> dict[str, object]:
idempotency = _sha("itworx.repopulse-legacy-adoption/v1:jens/repopulse:cli-test")
projection = _projection()
return {
"intentId": f"rpa-{idempotency[:20]}",
"idempotencySha256": idempotency,
"preflightSha256": _canonical_sha(projection),
"preflightProjection": projection,
"targetCredential": "A" * 64,
}
def _raw(payload: dict[str, object] | None = None) -> bytes:
return json.dumps(payload or _payload(), separators=(",", ":")).encode("utf-8")
def _core():
return cli._load_core_modules()
def _parsed(payload: dict[str, object] | None = None):
core = _core()
return cli.parse_handoff(_raw(payload), core.executor)
def test_parser_accepts_exact_handoff_and_rejects_duplicate_keys_at_any_depth():
core = _core()
payload = _payload()
parsed = cli.parse_handoff(_raw(payload), core.executor)
assert parsed.intent_id == payload["intentId"]
assert parsed.target_credential == b"A" * 64
assert parsed.preflight_projection == payload["preflightProjection"]
regular = _raw(payload)
duplicate_top = (
b'{"intentId":'
+ json.dumps(payload["intentId"]).encode("ascii")
+ bytes([44])
+ regular[1:]
)
with pytest.raises(cli.ControllerCliError) as error:
cli.parse_handoff(duplicate_top, core.executor)
assert error.value.code == "controller_handoff_duplicate_key"
duplicate_nested = regular.replace(b'"queued":0', b'"queued":0,"queued":0', 1)
with pytest.raises(cli.ControllerCliError) as error:
cli.parse_handoff(duplicate_nested, core.executor)
assert error.value.code == "controller_handoff_duplicate_key"
@pytest.mark.parametrize(
"change",
[
lambda value: value.pop("targetCredential"),
lambda value: value.update({"extra": True}),
lambda value: value.update({"targetCredential": "short"}),
lambda value: value.update({"targetCredential": "x" * 48 + "\n"}),
],
)
def test_parser_rejects_schema_changes_and_noncontroller_credentials(change):
core = _core()
payload = _payload()
change(payload)
with pytest.raises(cli.ControllerCliError):
cli.parse_handoff(_raw(payload), core.executor)
def test_parser_enforces_bounded_stdin_size():
core = _core()
with pytest.raises(cli.ControllerCliError) as error:
cli.parse_handoff(b"x" * (cli._MAX_HANDOFF_BYTES + 1), core.executor)
assert error.value.code == "controller_handoff_too_large"
def test_reserved_binding_requires_existing_matching_intent_and_credential():
core = _core()
handoff = _parsed()
binding = core.store.intent_binding_sha256(
handoff.intent_id,
handoff.idempotency_sha256,
handoff.preflight_sha256,
)
record = {
"intentId": handoff.intent_id,
"idempotencySha256": handoff.idempotency_sha256,
"preflightSha256": handoff.preflight_sha256,
"intentBindingSha256": binding,
"expectedTargetCredentialSha256": _sha(handoff.target_credential),
"status": "in_progress",
"registration": None,
}
class Store:
def read_only_status(self):
return {
"status": record["status"],
"hashes": {
"idempotencySha256": record["idempotencySha256"],
"preflightSha256": record["preflightSha256"],
"intentBindingSha256": record["intentBindingSha256"],
},
}
def load_intent(self):
return record
assert cli._require_reserved_binding(
store=Store(), store_module=core.store, handoff=handoff
) is record
mismatched = replace(handoff, preflight_sha256=_sha("another preflight"))
with pytest.raises(cli.ControllerCliError) as error:
cli._require_reserved_binding(
store=Store(), store_module=core.store, handoff=mismatched
)
assert error.value.code == "controller_intent_binding_mismatch"
wrong_credential = replace(handoff, target_credential=b"B" * 64)
with pytest.raises(cli.ControllerCliError) as error:
cli._require_reserved_binding(
store=Store(), store_module=core.store, handoff=wrong_credential
)
assert error.value.code == "target_credential_binding_mismatch"
def test_missing_reservation_is_rejected_before_loading_or_creating_journal():
core = _core()
handoff = _parsed()
class Store:
def read_only_status(self):
return None
def load_intent(self):
raise AssertionError("missing reservation must not load mutable state")
with pytest.raises(cli.ControllerCliError) as error:
cli._require_reserved_binding(
store=Store(), store_module=core.store, handoff=handoff
)
assert error.value.code == "controller_intent_not_reserved"
def test_fixed_storage_bridge_forwards_executor_revision_and_keeps_credential_in_memory():
handoff = _parsed()
class StorageHooks:
def __init__(self):
self.calls = []
def ensure_quiesced(self, **kwargs):
self.calls.append(("quiesce", kwargs))
return {"ok": True}
def create_or_verify_fresh_snapshot(self, **kwargs):
self.calls.append(("snapshot", kwargs))
return {"ok": True}
def copy_and_verify_managed_data(self, **kwargs):
self.calls.append(("copy", kwargs))
return {"ok": True}
hooks = StorageHooks()
bridge = cli._FixedHandoffStorageEffects(
store=object(),
store_module=SimpleNamespace(REPOSITORY="jens/repopulse"),
storage_effects=hooks,
legacy_rollback=object(),
registry_restore=object(),
handoff=handoff,
)
revision = handoff.preflight_projection["main_revision"]
binding = "c" * 64
bridge.ensure_quiesced(
intent_id=handoff.intent_id,
intent_binding_sha256=binding,
expected_main_revision=revision,
expected_container_id_sha256="d" * 64,
expected_container_image_sha256="e" * 64,
)
bridge.create_or_verify_fresh_snapshot(
intent_id=handoff.intent_id,
intent_binding_sha256=binding,
expected_main_revision=revision,
expected_snapshot=None,
)
bridge.copy_and_verify_managed_data(
intent_id=handoff.intent_id,
expected_main_revision=revision,
snapshot_evidence={"intentId": handoff.intent_id},
)
assert [call[0] for call in hooks.calls] == ["quiesce", "snapshot", "copy"]
assert all(call[1]["expected_main_revision"] == revision for call in hooks.calls)
assert bridge.registration_material(intent_id=handoff.intent_id) == (
f"enr-{handoff.intent_id[4:]}",
b"A" * 64,
)
with pytest.raises(cli.ControllerCliError):
bridge.registration_material(intent_id="rpa-" + "f" * 20)
def test_executor_store_proxy_snapshots_registry_before_registration_cas():
events = []
class Store:
def register_target(self, **kwargs):
events.append(("register", kwargs))
return {"status": "registered"}
def read_only_status(self):
return {"status": "in_progress"}
class RegistryRestore:
def preserve_before_registry_cas(self, store):
assert isinstance(store, Store)
events.append(("preserve", None))
proxy = cli._RegistrySnapshotStoreProxy(Store(), RegistryRestore())
assert proxy.register_target(intent_id="rpa-" + "a" * 20) == {
"status": "registered"
}
assert proxy.read_only_status() == {"status": "in_progress"}
assert [name for name, _value in events] == ["preserve", "register"]
def test_serialized_result_rejects_secret_leaks_and_unexpected_fields():
credential = b"A" * 64
intent_id = "rpa-" + "a" * 20
result = {key: None for key in cli._RESULT_KEYS}
result.update(
{
"apiVersion": "itworx.repopulse-controller-adoption-execution-result/v1",
"repository": "jens/repopulse",
"applicationId": "repopulse",
"intentId": intent_id,
"status": "deployment_verified",
"phase": "completion_verified",
"legacyVolume": "repopulse_data",
"legacyVolumeRetained": True,
}
)
result["deploymentReceipt"] = {"note": credential.decode("ascii")}
with pytest.raises(cli.ControllerCliError) as error:
cli._serialize_result(
result, target_credential=credential, intent_id=intent_id
)
assert error.value.code == "controller_result_secret_detected"
result["deploymentReceipt"] = {}
result["targetCredential"] = credential.decode("ascii")
with pytest.raises(cli.ControllerCliError) as error:
cli._serialize_result(
result, target_credential=credential, intent_id=intent_id
)
assert error.value.code == "controller_result_schema_invalid"
def test_disabled_controller_does_not_read_handoff_or_load_effects():
core = cli.CoreModules(
store=SimpleNamespace(EXECUTION_ENABLED=False),
executor=SimpleNamespace(EXECUTION_ENABLED=False),
)
effects_loaded = False
root_checked = False
class UnreadableInput:
def read(self, _size):
raise AssertionError("disabled controller must not read stdin")
def load_effects(_core):
nonlocal effects_loaded
effects_loaded = True
raise AssertionError("disabled controller must not construct effects")
def require_root():
nonlocal root_checked
root_checked = True
output = io.StringIO()
code = cli._run_cli(
argv=(),
stdin=UnreadableInput(),
stdout=output,
load_core=lambda: core,
load_effects=load_effects,
require_root=require_root,
)
assert code == 2
assert json.loads(output.getvalue()) == {
"status": "disabled",
"reason": "controller_adoption_execution_disabled",
}
assert not effects_loaded
assert root_checked
def test_nonroot_controller_rejects_before_loading_modules_or_reading_handoff():
core_loaded = False
class UnreadableInput:
def read(self, _size):
raise AssertionError("nonroot controller must not read stdin")
def load_core():
nonlocal core_loaded
core_loaded = True
raise AssertionError("nonroot controller must not load modules")
def require_root():
raise cli.ControllerCliError("controller_root_required")
output = io.StringIO()
code = cli._run_cli(
argv=(),
stdin=UnreadableInput(),
stdout=output,
load_core=load_core,
require_root=require_root,
)
assert code == 2
assert json.loads(output.getvalue()) == {
"status": "blocked",
"reason": "controller_root_required",
}
assert not core_loaded
def test_real_fixed_effect_modules_compose_without_side_effects():
core = cli._load_core_modules()
effects = cli._load_effect_modules(core)
handoff = _parsed()
store = core.store.RepoPulseAdoptionStore()
executor = cli._compose_executor(
core=core,
effects=effects,
store=store,
handoff=handoff,
)
assert isinstance(executor, core.executor.RepoPulseAdoptionExecutor)
assert executor._effects.__class__.__name__ == "_RevisionBoundRuntimeEffects"