← back to Unclaimed Property Platform

tests/test_cycle9_claims_persistence.py

109 lines

"""Cycle 8 test — claim lifecycle against a REAL SQLite ClaimRepository.

Run:  python -m tests.test_cycle9_claims_persistence

Proves the claims subsystem is runnable end-to-end on real persistence (not a mock):
  1. full lifecycle DRAFT -> ... -> SUBMITTED_TO_STATE persists across get_for_update reads;
  2. queue+complete write the outbox event and both status events WITHOUT colliding on the
     real UNIQUE(claim_id, idempotency_key) constraint (C3 fix proven against the DB);
  3. the DB itself enforces idempotency (duplicate key -> IntegrityError);
  4. the state-only guard blocks claimant self-approval on a persisted claim;
  5. illegal transitions raise.
"""
from __future__ import annotations

import sqlite3
import sys
import tempfile
from pathlib import Path
from uuid import uuid4

sys.path.insert(0, str(Path(__file__).resolve().parents[1]))

from services.claims.claim_workflow import (
    ClaimStatus, complete_state_submission, queue_state_submission, transition_claim,
)
from services.claims.sqlite_claim_repo import SqliteClaimRepository
from services.common.sqlite_repo import SqliteRepository


class FakeStateAdapter:
    def submit_claim(self, claim, idempotency_key) -> str:
        return "STATE-CASE-1"


def ok(msg: str) -> None:
    print(f"  ✓ {msg}")


def main() -> int:
    tmp = Path(tempfile.mkdtemp(prefix="upp-cycle9-"))
    base = SqliteRepository(str(tmp / "p.db"))       # creates full schema
    repo = SqliteClaimRepository(base.conn)          # shares the one system-of-record DB

    print("1) create + walk the lifecycle (persisted)")
    claim = repo.create_claim("SAMPLE", public_property_reference="ref-1", claimant_id=uuid4())
    assert repo.get_for_update(claim.claim_id).status == ClaimStatus.DRAFT
    steps = [
        (ClaimStatus.IDENTITY_PENDING, "k1"),
        (ClaimStatus.EVIDENCE_PENDING, "k2"),
        (ClaimStatus.READY_FOR_SUBMISSION, "k3"),
    ]
    for target, key in steps:
        transition_claim(repo, claim.claim_id, target, actor_id="worker", idempotency_key=key)
    assert repo.get_for_update(claim.claim_id).status == ClaimStatus.READY_FOR_SUBMISSION
    ok("DRAFT -> IDENTITY -> EVIDENCE -> READY_FOR_SUBMISSION persisted")

    print("2) queue + complete submission (real UNIQUE constraint, no collision)")
    queue_state_submission(repo, claim.claim_id, actor_id="worker", idempotency_key="sub")
    assert repo.get_for_update(claim.claim_id).status == ClaimStatus.SUBMITTING
    complete_state_submission(repo, FakeStateAdapter(), claim.claim_id, idempotency_key="sub")
    final = repo.get_for_update(claim.claim_id)
    assert final.status == ClaimStatus.SUBMITTED_TO_STATE, final.status
    assert final.state_case_id == "STATE-CASE-1", final.state_case_id
    outbox = repo.pending_outbox(claim.claim_id)
    assert any(e["event_type"] == "state_claim_submission_requested" for e in outbox), outbox
    ok(f"SUBMITTED_TO_STATE persisted; state_case_id set; outbox has request; "
       f"{len(repo.events(claim.claim_id))} events, keys all unique")

    print("3) DB itself enforces idempotency (duplicate key -> IntegrityError)")
    repo.append_event(claim.claim_id, "probe", {"n": 1}, idempotency_key="dupe")
    raised = False
    try:
        repo.append_event(claim.claim_id, "probe", {"n": 2}, idempotency_key="dupe")
    except sqlite3.IntegrityError:
        raised = True
    assert raised, "duplicate (claim_id, idempotency_key) must raise IntegrityError"
    ok("duplicate idempotency key rejected by the real UNIQUE constraint")

    print("4) state-only guard on a persisted claim")
    raised = False
    try:
        transition_claim(repo, claim.claim_id, ClaimStatus.APPROVED,
                         actor_id="claimant:self", idempotency_key="appr")
    except PermissionError:
        raised = True
    assert raised, "claimant must not self-approve"
    # a state actor can
    transition_claim(repo, claim.claim_id, ClaimStatus.APPROVED,
                     actor_id="state:CA", idempotency_key="appr2")
    assert repo.get_for_update(claim.claim_id).status == ClaimStatus.APPROVED
    ok("claimant approval blocked; state:CA approval persisted")

    print("5) illegal transition raises")
    raised = False
    try:
        transition_claim(repo, claim.claim_id, ClaimStatus.DRAFT,
                         actor_id="worker", idempotency_key="bad")
    except ValueError:
        raised = True
    assert raised, "APPROVED -> DRAFT is illegal"
    ok("illegal transition (APPROVED -> DRAFT) rejected")

    print("\nALL CYCLE-8 CLAIM-PERSISTENCE ASSERTIONS PASSED ✅")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())