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

513 lines
19 KiB
Python

from __future__ import annotations
import pytest
from backend.app.services.enrollment_profiles import PROFILE_VERSION
from backend.app.services.repopulse_legacy_adoption_protocol import (
APPLICATION_ID,
CONTROLLER_RECEIPT_VERSION,
EXECUTION_ENABLED,
LEGACY_VOLUME,
REPOSITORY,
AdoptionProtocolError,
advance_intent,
complete_intent,
create_intent,
execution_status,
intent_binding_sha256,
mark_recovery_required,
reserve_or_replay,
resume_intent,
)
def _sha(value: str) -> str:
return value * 64
def _passing_preflight() -> dict:
checks = [
{
"code": code,
"status": "passed",
"gate": "legacy_compatibility",
"detail": "sanitized fixed check",
}
for code in (
"legacy_registry",
"controller_state",
"gitea_ancestry",
"runtime_identity_and_health",
"legacy_storage_topology",
"legacy_appdata",
"product_api",
"database_health",
"queue_quiescence",
"database_baseline",
)
]
return {
"apiVersion": "itworx.repopulse-legacy-adoption-preflight/v1",
"repository": REPOSITORY,
"status": "preflight_passed",
"legacyCompatibility": {"status": "passed", "checks": checks},
"targetAcceptance": {"status": "blocked", "checks": []},
"readOnly": True,
"adoptionAuthorized": False,
"evidence": {
"main_revision": "a" * 40,
"live_revision": "b" * 40,
"container_id_sha256": _sha("d"),
"container_image_sha256": _sha("1"),
"container_state": "running",
"container_health": "healthy",
"queue": {"queued": 0, "active": 0, "completed": 569, "failed": 0},
"database": {
"database_bytes": 44_576_768,
"journal_bytes": 0,
"scan_count": 572,
"compacted_scan_count": 96,
},
},
}
def _intent() -> dict:
return create_intent(_passing_preflight(), "repopulse-adoption-2026")
def _target_gates() -> dict:
return {
"profileId": "repopulse-authenticated",
"profileVersion": PROFILE_VERSION,
"planSha256": _sha("c"),
"profileReady": True,
"controllerTrustReady": True,
"controllerBootstrapSecretImported": True,
"typedEnrollmentTrustReady": True,
"targetSecretReady": True,
"exactPolicyReady": True,
"authenticatedRouteReady": True,
"accessListReady": True,
"mutationBoundaryReady": True,
"managedRuntimeReady": True,
"scanSourceMatches": True,
"scanMountReadOnly": True,
"scanPermissionPolicyPersistent": True,
"scanAccessForRuntimeIdentityReady": True,
"ownedDataTargetReady": True,
"managedEnvironmentReady": True,
"legacyEnvironmentRetained": True,
"maintenanceAuthReady": True,
"controllerAdoptionCapabilityReady": True,
"controllerRegistryWriterReady": True,
"controllerSecretWriterReady": True,
}
def _quiescence(intent: dict) -> dict:
return {
"intentId": intent["intentId"],
"globalDeployLockHeld": True,
"projectLockHeld": True,
"maintenanceModeActive": True,
"queueFrozen": True,
"queuedJobs": 0,
"activeJobs": 0,
"maintenanceModeAuthenticated": True,
"containerName": "repopulse",
"containerIdSha256": _sha("d"),
"containerImageSha256": _sha("1"),
"containerState": "exited",
"legacyVolume": LEGACY_VOLUME,
"controllerIntentDurable": True,
"controllerIntentBindingSha256": intent_binding_sha256(intent),
}
def _snapshot(intent: dict) -> dict:
return {
"intentId": intent["intentId"],
"globalDeployLockHeld": True,
"projectLockHeld": True,
"legacyContainerStopped": True,
"legacyVolume": LEGACY_VOLUME,
"legacyVolumeRetained": True,
"sqliteBackupMethod": "sqlite_online_backup",
"quickCheck": "ok",
"integrityCheck": "ok",
"sourceLogicalSha256": _sha("e"),
"snapshotLogicalSha256": _sha("e"),
"snapshotFileSha256": _sha("f"),
"sourceScanCount": 572,
"snapshotScanCount": 572,
"sourceCompactedScanCount": 96,
"snapshotCompactedScanCount": 96,
"sourceQueueSha256": _sha("1"),
"snapshotQueueSha256": _sha("1"),
"sourceAuxiliaryTreeSha256": _sha("3"),
"sourceAuxiliaryFileCount": 42,
"targetDataPathWasAbsent": True,
"snapshotReceiptDurable": True,
"legacyEnvironmentRetained": True,
"managedEnvironmentFilePrivate": True,
"controllerIntentBindingSha256": intent_binding_sha256(intent),
}
def _registration(intent: dict) -> dict:
gates = intent["evidence"]["targetGates"]
snapshot = intent["evidence"]["snapshot"]
return {
"receiptVersion": CONTROLLER_RECEIPT_VERSION,
"intentId": intent["intentId"],
"receiptSha256": _sha("2"),
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"enrollmentId": "enr-0123456789abcdefabcd",
"profileId": gates["profileId"],
"planSha256": gates["planSha256"],
"snapshotFileSha256": snapshot["snapshotFileSha256"],
"targetLogicalSha256": snapshot["snapshotLogicalSha256"],
"targetScanCount": snapshot["snapshotScanCount"],
"targetCompactedScanCount": snapshot["snapshotCompactedScanCount"],
"targetQueueSha256": snapshot["snapshotQueueSha256"],
"targetAuxiliaryTreeSha256": snapshot["sourceAuxiliaryTreeSha256"],
"targetAuxiliaryFileCount": snapshot["sourceAuxiliaryFileCount"],
"targetQueuedJobs": 0,
"targetActiveJobs": 0,
"targetQuickCheck": "ok",
"targetIntegrityCheck": "ok",
"targetRecoveryStatus": "healthy",
"targetDataUid": 10001,
"targetDataGid": 10001,
"managedDataMountMatches": True,
"copySourceIsVerifiedSnapshot": True,
"registryTargetMatches": True,
"controllerStateReceiptVerified": True,
"deploymentReceiptSchema": "gitea-deploy-receipt.v1",
"deploymentReceiptSha256": _sha("4"),
"deploymentReceiptVerified": True,
"runtimeHealthy": True,
"publishedHostPorts": [],
"globalDeployLockHeld": True,
"projectLockHeld": True,
"legacyVolume": LEGACY_VOLUME,
"legacyVolumeRetained": True,
"legacyEnvironmentRetained": True,
"managedEnvironmentFilePrivate": True,
"scanSourceMatches": True,
"scanMountReadOnly": True,
"scanPermissionPolicyPersistent": True,
"scanAccessForRuntimeIdentityReady": True,
"controllerIntentDurable": True,
"controllerIntentBindingSha256": intent_binding_sha256(intent),
}
def _to_snapshot_verified() -> dict:
intent = advance_intent(_intent(), "target_gates", _target_gates())
intent = advance_intent(intent, "quiescence", _quiescence(intent))
return advance_intent(intent, "snapshot", _snapshot(intent))
def _to_target_registered() -> dict:
intent = _to_snapshot_verified()
return advance_intent(intent, "controller_registration", _registration(intent))
def test_passing_legacy_preflight_can_reserve_but_never_authorizes_adoption() -> None:
intent = _intent()
assert intent["repository"] == REPOSITORY
assert intent["applicationId"] == APPLICATION_ID
assert intent["legacyVolume"] == LEGACY_VOLUME
assert intent["phase"] == "reserved"
assert intent["status"] == "in_progress"
assert "adoptionAuthorized" not in intent
assert intent["evidence"]["preflight"]["queue"]["queued"] == 0
def test_reservation_is_idempotent_and_binds_the_preflight_facts() -> None:
first = create_intent(_passing_preflight(), "repopulse-adoption-2026")
replay = reserve_or_replay(first, _passing_preflight(), "repopulse-adoption-2026")
changed = _passing_preflight()
changed["evidence"]["main_revision"] = "c" * 40
assert replay == first
with pytest.raises(AdoptionProtocolError, match="idempotency_conflict"):
reserve_or_replay(first, changed, "repopulse-adoption-2026")
with pytest.raises(AdoptionProtocolError, match="one_time_adoption_already_reserved"):
reserve_or_replay(first, _passing_preflight(), "repopulse-adoption-2027")
assert intent_binding_sha256(first) == intent_binding_sha256(replay)
def test_modified_persisted_intent_evidence_fails_integrity_check() -> None:
intent = _intent()
intent["evidence"]["preflight"]["repository"] = "jens/other"
with pytest.raises(AdoptionProtocolError, match="intent_evidence_invalid"):
advance_intent(intent, "target_gates", _target_gates())
@pytest.mark.parametrize(
"mutate,code",
[
(lambda report: report.update(repository="jens/other"), "legacy_preflight_not_compatible"),
(lambda report: report.update(adoptionAuthorized=True), "legacy_preflight_not_compatible"),
(lambda report: report.update(status="blocked"), "legacy_preflight_not_compatible"),
(lambda report: report["evidence"]["queue"].update(active=1), "legacy_preflight_queue_not_quiescent"),
(lambda report: report["legacyCompatibility"]["checks"].pop(), "legacy_preflight_check_incomplete"),
],
)
def test_invalid_or_tampered_preflight_cannot_reserve(mutate, code: str) -> None:
report = _passing_preflight()
mutate(report)
with pytest.raises(AdoptionProtocolError) as error:
create_intent(report, "repopulse-adoption-2026")
assert error.value.code == code
def test_changed_or_unready_profile_trust_secret_acl_or_runtime_gate_blocks() -> None:
intent = _intent()
gates = _target_gates()
gates["accessListReady"] = False
with pytest.raises(AdoptionProtocolError, match="target_gates_not_ready"):
advance_intent(intent, "target_gates", gates)
with pytest.raises(AdoptionProtocolError, match="adoption_step_out_of_order"):
advance_intent(intent, "quiescence", _quiescence(intent))
@pytest.mark.parametrize(
("field", "value"),
[("profileId", "internal-webapp"), ("profileVersion", "unreviewed/v9")],
)
def test_target_gates_require_the_exact_managed_profile(field: str, value: str) -> None:
gates = _target_gates()
gates[field] = value
with pytest.raises(AdoptionProtocolError, match="target_gates_not_ready"):
advance_intent(_intent(), "target_gates", gates)
def test_quiescence_requires_locks_maintenance_drain_stop_and_the_named_volume() -> None:
intent = advance_intent(_intent(), "target_gates", _target_gates())
evidence = _quiescence(intent)
evidence["globalDeployLockHeld"] = False
with pytest.raises(AdoptionProtocolError, match="quiescence_not_proven"):
advance_intent(intent, "quiescence", evidence)
evidence = _quiescence(intent)
evidence["activeJobs"] = 1
with pytest.raises(AdoptionProtocolError, match="quiescence_not_proven"):
advance_intent(intent, "quiescence", evidence)
evidence = _quiescence(intent)
evidence["controllerIntentBindingSha256"] = _sha("9")
with pytest.raises(AdoptionProtocolError, match="controller_intent_binding_mismatch"):
advance_intent(intent, "quiescence", evidence)
evidence = _quiescence(intent)
evidence["containerIdSha256"] = _sha("9")
with pytest.raises(AdoptionProtocolError, match="quiescence_not_proven"):
advance_intent(intent, "quiescence", evidence)
def test_quiesced_snapshot_requires_sqlite_online_backup_and_matching_logical_data() -> None:
intent = advance_intent(_intent(), "target_gates", _target_gates())
intent = advance_intent(intent, "quiescence", _quiescence(intent))
evidence = _snapshot(intent)
evidence["sqliteBackupMethod"] = "raw_file_copy"
with pytest.raises(AdoptionProtocolError, match="sqlite_snapshot_not_verified"):
advance_intent(intent, "snapshot", evidence)
evidence = _snapshot(intent)
evidence["sourceLogicalSha256"] = _sha("a")
with pytest.raises(AdoptionProtocolError, match="sqlite_snapshot_not_verified"):
advance_intent(intent, "snapshot", evidence)
def test_controller_receipt_must_bind_exact_target_keep_legacy_volume_and_have_no_ports() -> None:
intent = _to_snapshot_verified()
evidence = _registration(intent)
evidence["publishedHostPorts"] = [{"ip": "0.0.0.0", "port": 1254}]
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
advance_intent(intent, "controller_registration", evidence)
evidence = _registration(intent)
evidence["legacyVolumeRetained"] = False
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
advance_intent(intent, "controller_registration", evidence)
evidence = _registration(intent)
evidence["targetDataUid"] = 0
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
advance_intent(intent, "controller_registration", evidence)
evidence = _registration(intent)
evidence["targetActiveJobs"] = 1
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
advance_intent(intent, "controller_registration", evidence)
for field, invalid in (
("targetAuxiliaryTreeSha256", _sha("5")),
("targetAuxiliaryFileCount", 41),
("deploymentReceiptSchema", "unknown"),
("deploymentReceiptSha256", "missing"),
("deploymentReceiptVerified", False),
):
evidence = _registration(intent)
evidence[field] = invalid
with pytest.raises(AdoptionProtocolError, match="controller_adoption_receipt_not_verified"):
advance_intent(intent, "controller_registration", evidence)
def test_exact_controller_and_appops_ownership_are_separate_checkpoints() -> None:
intent = _to_target_registered()
registration = intent["evidence"]["controllerRegistration"]
gates = intent["evidence"]["targetGates"]
evidence = {
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"enrollmentId": registration["enrollmentId"],
"profileId": registration["profileId"],
"planSha256": gates["planSha256"],
"controllerReceiptSha256": registration["receiptSha256"],
"enrollmentPersisted": True,
"exactApplyPolicyPersisted": True,
"exactRollbackPolicyPersisted": True,
"secretReferenceReady": True,
}
appops_owned = advance_intent(intent, "appops_ownership", evidence)
complete = complete_intent(
appops_owned,
{
"intentId": intent["intentId"],
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"enrollmentId": registration["enrollmentId"],
"controllerReceiptSha256": registration["receiptSha256"],
"enrollmentStillPersisted": True,
"exactApplyPolicyStillPersisted": True,
"exactRollbackPolicyStillPersisted": True,
"controllerReceiptStillMatches": True,
"legacyVolumeRetained": True,
},
)
assert complete["phase"] == "completed"
assert complete["status"] == "completed"
assert "appopsOwnership" in complete["evidence"]
assert "completion" in complete["evidence"]
def test_step_replay_is_idempotent_but_changed_evidence_is_a_conflict() -> None:
intent = advance_intent(_intent(), "target_gates", _target_gates())
replay = advance_intent(intent, "target_gates", _target_gates())
changed = _target_gates()
changed["planSha256"] = _sha("9")
assert replay == intent
with pytest.raises(AdoptionProtocolError, match="idempotency_conflict"):
advance_intent(intent, "target_gates", changed)
def test_recovery_marker_keeps_last_good_phase_and_never_deletes_legacy_data() -> None:
intent = _to_snapshot_verified()
failed = mark_recovery_required(
intent,
"registry_receipt_uncertain",
{"rawError": "SECRET_OR_PATH_MUST_NOT_BE_STORED"},
)
assert failed["status"] == "recovery_required"
assert failed["phase"] == "snapshot_verified"
assert failed["legacyVolumePreserved"] is True
assert "legacyVolumePreserved" not in failed["recovery"] or failed["recovery"]["legacyVolumePreserved"] is True
assert "SECRET_OR_PATH_MUST_NOT_BE_STORED" not in str(failed)
with pytest.raises(AdoptionProtocolError, match="intent_requires_recovery"):
advance_intent(failed, "controller_registration", _registration(intent))
def test_recovery_reconciles_only_exact_controller_committed_registration() -> None:
intent = _to_snapshot_verified()
failed = mark_recovery_required(intent, "appops_commit_uncertain", {"timeout": True})
registration = _registration(intent)
receipt = {
"apiVersion": CONTROLLER_RECEIPT_VERSION,
"intentId": intent["intentId"],
"phase": "target_registered",
"receiptSha256": registration["receiptSha256"],
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"legacyVolumeRetained": True,
"intentDurable": True,
"intentBindingSha256": intent_binding_sha256(intent),
"registrationEvidence": registration,
}
resumed = resume_intent(failed, receipt)
assert resumed["status"] == "in_progress"
assert resumed["phase"] == "target_registered"
assert resumed["evidence"]["controllerRegistration"]["receiptSha256"] == registration["receiptSha256"]
def test_recovery_replays_registered_target_without_repeating_cutover() -> None:
intent = _to_target_registered()
failed = mark_recovery_required(intent, "appops_commit_uncertain", {"commit": "timeout"})
registration = intent["evidence"]["controllerRegistration"]
receipt = {
"apiVersion": CONTROLLER_RECEIPT_VERSION,
"intentId": intent["intentId"],
"phase": "target_registered",
"receiptSha256": registration["receiptSha256"],
"repository": REPOSITORY,
"applicationId": APPLICATION_ID,
"legacyVolumeRetained": True,
"intentDurable": True,
"intentBindingSha256": intent_binding_sha256(intent),
"registrationEvidence": registration,
}
resumed = resume_intent(failed, receipt)
assert resumed["phase"] == "target_registered"
assert resumed["status"] == "in_progress"
assert resumed["receipts"]["controller_registration"] == intent["receipts"]["controller_registration"]
def test_recovery_receipt_for_another_repository_or_intent_is_rejected() -> None:
intent = mark_recovery_required(_to_snapshot_verified(), "registry_receipt_uncertain", {})
receipt = {
"apiVersion": CONTROLLER_RECEIPT_VERSION,
"intentId": "rpa-00000000000000000000",
"phase": "target_registered",
"receiptSha256": _sha("2"),
"repository": "jens/other",
"applicationId": APPLICATION_ID,
"legacyVolumeRetained": True,
"intentDurable": True,
"intentBindingSha256": intent_binding_sha256(intent),
"registrationEvidence": {},
}
with pytest.raises(AdoptionProtocolError, match="controller_recovery_receipt_unbound"):
resume_intent(intent, receipt)
def test_protocol_activation_reports_enabled_without_claiming_readiness() -> None:
assert EXECUTION_ENABLED is True
assert execution_status() == {
"apiVersion": "itworx.repopulse-legacy-adoption/v1",
"repository": REPOSITORY,
"status": "enabled",
"reason": None,
"executionEnabled": True,
}