Files
Unraid-AppOps-Gateway-Public/backend/tests/test_repopulse_adoption_ownership.py

607 lines
21 KiB
Python

from __future__ import annotations
import hashlib
import json
import pytest
from sqlmodel import Session, SQLModel, create_engine, select
from backend.app.db.models import (
AppSetting,
Enrollment,
Policy,
RepoPulseAdoptionJournal,
)
from backend.app.schemas.enrollment import EnrollmentPlanV1
from backend.app.services import repopulse_adoption_journal as journal
from backend.app.services import repopulse_adoption_ownership as ownership
from backend.app.services.repopulse_legacy_adoption_protocol import (
APPLICATION_ID,
CONTROLLER_RECEIPT_VERSION,
LEGACY_VOLUME,
REPOSITORY,
intent_binding_sha256,
)
from backend.app.services.secret_store import set_secret
def _sha(char: str) -> str:
return char * 64
def _plan_payload(revision: str) -> dict:
return EnrollmentPlanV1.model_validate(
{
"application_id": "repopulse",
"display_name": "RepoPulse",
"environment": "production",
"repository": "Jens/RepoPulse",
"revision": revision,
"profile": "repopulse-authenticated",
"target": {
"target_identity": "unraid-controller",
"container_name": "repopulse",
},
"storage": {"runtime_uid": 10001, "runtime_gid": 10001},
"network": {"docker_network": "itworx-mcp-hub-npm", "internal_port": 8000},
"artifact": {
"release_version": "1.5.0",
"image_repository": "repopulse-managed",
"image_tag": "production",
"health_path": "/health",
},
"exposure": {
"hostname": "repopulse.example.com",
"upstream_service": "repopulse",
},
"secrets": {"additional_secret_refs": ["REPOPULSE_GITEA_TOKEN"]},
}
).model_dump(mode="json")
def _plan_sha(revision: str) -> str:
payload = _plan_payload(revision)
canonical = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode()
return hashlib.sha256(canonical).hexdigest()
def _preflight() -> dict:
check_codes = (
"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": [
{"code": code, "status": "passed", "gate": "legacy_compatibility"}
for code in check_codes
],
},
"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 _target_gates() -> dict:
return {
"profileId": "repopulse-authenticated",
"profileVersion": "enrollment-profile/v1",
"planSha256": _plan_sha("a" * 40),
**{
key: True
for key in (
"profileReady",
"controllerTrustReady",
"controllerBootstrapSecretImported",
"typedEnrollmentTrustReady",
"targetSecretReady",
"exactPolicyReady",
"authenticatedRouteReady",
"accessListReady",
"mutationBoundaryReady",
"managedRuntimeReady",
"scanSourceMatches",
"scanMountReadOnly",
"scanPermissionPolicyPersistent",
"scanAccessForRuntimeIdentityReady",
"ownedDataTargetReady",
"managedEnvironmentReady",
"legacyEnvironmentRetained",
"maintenanceAuthReady",
"controllerAdoptionCapabilityReady",
"controllerRegistryWriterReady",
"controllerSecretWriterReady",
)
},
}
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, *, enrollment_id: str | None = None) -> 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": enrollment_id or f"enr-{intent['intentId'][4:]}",
"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 _ready_engine():
engine = create_engine("sqlite://")
SQLModel.metadata.create_all(engine)
with Session(engine) as session:
session.add(
AppSetting(
key="deployment_enrollment:trust",
is_secret=False,
value_json={
"controller_identity": "unraid-controller",
"bootstrap_secret_ref": "ENROLLMENT_BOOTSTRAP",
"approved_networks": ["itworx-mcp-hub-npm"],
"allowed_domain_suffixes": ["example.com"],
"autonomous_profiles": ["repopulse-authenticated"],
"repopulse_proxy_access_list_id": 1,
},
)
)
set_secret(
session,
"ENROLL_REPOPULSE_TARGET_HMAC",
"test-target-credential-only-for-fixture",
commit=False,
)
session.commit()
return engine
def _register_target(
engine, *, enrollment_id: str | None = None
) -> tuple[dict, dict, int]:
preflight = _preflight()
with Session(engine) as session:
reserved = journal.reserve_repopulse_adoption(
session, preflight, "repopulse-adoption-2026"
)
intent = journal.protocol.create_intent(preflight, "repopulse-adoption-2026")
gates = _target_gates()
with Session(engine) as session:
journal.advance_repopulse_adoption(
session, "target_gates", gates, expected_revision=reserved["revision"]
)
intent = journal.protocol.advance_intent(intent, "target_gates", gates)
quiescence = _quiescence(intent)
with Session(engine) as session:
journal.confirm_repopulse_controller_intent(
session, quiescence, expected_revision=2
)
intent = journal.protocol.advance_intent(intent, "quiescence", quiescence)
snapshot = _snapshot(intent)
with Session(engine) as session:
journal.advance_repopulse_adoption(
session, "snapshot", snapshot, expected_revision=3
)
intent = journal.protocol.advance_intent(intent, "snapshot", snapshot)
registration = _registration(intent, enrollment_id=enrollment_id)
with Session(engine) as session:
registered = journal.advance_repopulse_adoption(
session,
"controller_registration",
registration,
expected_revision=4,
)
return intent, registration, registered["revision"]
@pytest.fixture
def engine():
db = _ready_engine()
yield db
db.dispose()
def test_ownership_commit_is_atomic_fixed_and_replayable(engine) -> None:
intent, registration, revision = _register_target(engine)
with Session(engine) as session:
result = ownership.commit_repopulse_adoption_ownership(
session,
registration,
expected_revision=revision,
)
assert result["phase"] == "appops_owned"
assert result["revision"] == revision + 1
with Session(engine) as session:
row = session.get(Enrollment, registration["enrollmentId"])
assert row is not None
assert row.repository == "jens/repopulse"
assert row.application_id == "repopulse"
assert row.revision == "a" * 40
assert row.profile == "repopulse-authenticated"
assert row.controller_identity == "unraid-controller"
assert row.plan_sha256 == registration["planSha256"]
assert row.target_secret_ref == "ENROLL_REPOPULSE_TARGET_HMAC"
assert row.oidc_secret_ref is None
assert row.plan_json == _plan_payload("a" * 40)
assert row.provenance_json["createdBy"] == "repopulse-legacy-adoption/v1"
assert (
row.provenance_json["controllerReceiptSha256"]
== registration["receiptSha256"]
)
policies = session.exec(select(Policy).order_by(Policy.id)).all()
assert [policy.id for policy in policies] == [
"enrollment-repopulse-deployment-apply",
"enrollment-repopulse-deployment-rollback",
]
assert {
(item.action, item.target, item.voice, item.manual, item.risk)
for item in policies
} == {
("deployment.apply", "repopulse", "blocked", "allowed", "high"),
("deployment.rollback", "repopulse", "blocked", "allowed", "high"),
}
assert all(
item.provenance_json["adoptionIntentId"] == intent["intentId"]
for item in policies
)
stored_journal = session.get(RepoPulseAdoptionJournal, "repopulse")
assert stored_journal.phase == "appops_owned"
assert (
stored_journal.intent_json["evidence"]["appopsOwnership"][
"secretReferenceReady"
]
is True
)
with Session(engine) as session:
replay = ownership.commit_repopulse_adoption_ownership(
session,
registration,
expected_revision=revision + 1,
)
assert replay["phase"] == "appops_owned"
assert replay["revision"] == revision + 1
def test_ownership_commit_rejects_live_controller_receipt_drift(engine) -> None:
_, registration, revision = _register_target(engine)
changed = {**registration, "deploymentReceiptSha256": _sha("5")}
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.commit_repopulse_adoption_ownership(
session, changed, expected_revision=revision
)
assert error.value.code == "idempotency_conflict"
with Session(engine) as session:
assert session.exec(select(Enrollment)).all() == []
assert session.exec(select(Policy)).all() == []
def test_enrollment_identity_is_derived_from_persisted_intent(engine) -> None:
_, registration, revision = _register_target(
engine, enrollment_id="enr-0123456789abcdefabcd"
)
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
assert error.value.code == "fixed_enrollment_id_mismatch"
with Session(engine) as session:
assert session.exec(select(Enrollment)).all() == []
assert session.exec(select(Policy)).all() == []
def test_preexisting_exact_policy_id_is_not_adopted(engine) -> None:
_, registration, revision = _register_target(engine)
with Session(engine) as session:
session.add(
Policy(
id="enrollment-repopulse-deployment-apply",
action="deployment.apply",
target="repopulse",
voice="blocked",
manual="denied",
risk="high",
)
)
session.commit()
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
assert error.value.code == "ownership_resource_conflict"
with Session(engine) as session:
assert session.exec(select(Enrollment)).all() == []
assert (
session.get(Policy, "enrollment-repopulse-deployment-apply").manual
== "denied"
)
assert session.get(Policy, "enrollment-repopulse-deployment-rollback") is None
assert (
session.get(RepoPulseAdoptionJournal, "repopulse").phase
== "target_registered"
)
def test_ownership_commit_rejects_missing_fixed_secret_ref(engine) -> None:
_, registration, revision = _register_target(engine)
with Session(engine) as session:
from backend.app.db.models import Secret
session.delete(session.get(Secret, "ENROLL_REPOPULSE_TARGET_HMAC"))
session.commit()
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
assert error.value.code == "target_secret_reference_unavailable"
with Session(engine) as session:
assert session.exec(select(Enrollment)).all() == []
assert session.exec(select(Policy)).all() == []
def test_completion_is_a_separate_idempotent_step_bound_to_live_receipt(engine) -> None:
_, registration, revision = _register_target(engine)
with Session(engine) as session:
owned = ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
with Session(engine) as session:
completed = ownership.complete_repopulse_adoption(
session,
registration,
expected_revision=owned["revision"],
)
assert completed["phase"] == "completed"
assert completed["status"] == "completed"
assert completed["revision"] == owned["revision"] + 1
with Session(engine) as session:
replay = ownership.complete_repopulse_adoption(
session,
registration,
expected_revision=completed["revision"],
)
assert replay["revision"] == completed["revision"]
assert replay["phase"] == "completed"
with Session(engine) as session:
ownership_replay = ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=completed["revision"]
)
assert ownership_replay["revision"] == completed["revision"]
assert ownership_replay["phase"] == "completed"
changed = {**registration, "receiptSha256": _sha("9")}
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.complete_repopulse_adoption(
session, changed, expected_revision=completed["revision"]
)
assert error.value.code == "idempotency_conflict"
def test_completion_replay_rechecks_owned_policy_rows(engine) -> None:
_, registration, revision = _register_target(engine)
with Session(engine) as session:
owned = ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
with Session(engine) as session:
completed = ownership.complete_repopulse_adoption(
session, registration, expected_revision=owned["revision"]
)
with Session(engine) as session:
policy = session.get(Policy, "enrollment-repopulse-deployment-apply")
policy.manual = "denied"
session.add(policy)
session.commit()
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.complete_repopulse_adoption(
session, registration, expected_revision=completed["revision"]
)
assert error.value.code == "ownership_policy_readback_mismatch"
def test_half_write_failure_rolls_back_all_ownership_rows_and_marks_recovery(
engine, monkeypatch
) -> None:
_, registration, revision = _register_target(engine)
original_commit = Session.commit
calls = 0
def fail_first_commit(session):
nonlocal calls
calls += 1
if calls == 1:
raise RuntimeError("simulated commit interruption")
return original_commit(session)
monkeypatch.setattr(Session, "commit", fail_first_commit)
with (
Session(engine) as session,
pytest.raises(ownership.RepoPulseOwnershipError) as error,
):
ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
assert error.value.code == "appops_commit_uncertain"
with Session(engine) as session:
assert session.exec(select(Enrollment)).all() == []
assert session.exec(select(Policy)).all() == []
stored = session.get(RepoPulseAdoptionJournal, "repopulse")
assert stored.phase == "target_registered"
assert stored.status == "recovery_required"
assert stored.intent_json["recovery"]["reason"] == "appops_commit_uncertain"
def test_commit_exception_after_durable_commit_is_resolved_by_readback(
engine, monkeypatch
) -> None:
_, registration, revision = _register_target(engine)
original_commit = Session.commit
calls = 0
def commit_then_lose_response(session):
nonlocal calls
calls += 1
if calls == 1:
original_commit(session)
raise RuntimeError("simulated lost commit response")
return original_commit(session)
monkeypatch.setattr(Session, "commit", commit_then_lose_response)
with Session(engine) as session:
result = ownership.commit_repopulse_adoption_ownership(
session, registration, expected_revision=revision
)
assert result["phase"] == "appops_owned"
with Session(engine) as session:
assert session.get(Enrollment, registration["enrollmentId"]) is not None
stored = session.get(RepoPulseAdoptionJournal, "repopulse")
assert stored.phase == "appops_owned"
assert stored.status == "in_progress"