;; identity/tests/membership.sx — membership state machine + per-app ;; grant projection. Valid transitions advance state; invalid ones are ;; explicit errors. The projection renders one canonical state per app. (define id-membership-test-count 0) (define id-membership-test-pass 0) (define id-membership-test-fails (list)) (define id-membership-test (fn (name actual expected) (set! id-membership-test-count (+ id-membership-test-count 1)) (if (= actual expected) (set! id-membership-test-pass (+ id-membership-test-pass 1)) (append! id-membership-test-fails {:name name :expected expected :actual actual})))) (define idm-ev erlang-eval-ast) (define idmnm (fn (v) (get v :name))) (identity-load-membership!) ;; ── request → pending → approve → active ───────────────────────── (id-membership-test "request leaves the subject pending" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n case identity_membership:status(M, alice) of\n {ok, St, _} -> St;\n {none} -> none\n end")) "pending") (id-membership-test "approve activates a pending membership" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n case identity_membership:status(M, alice) of\n {ok, St, _} -> St;\n {none} -> none\n end")) "active") (id-membership-test "status keeps the requested tier" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, supporter),\n identity_membership:approve(M, alice),\n case identity_membership:status(M, alice) of\n {ok, _, Tier} -> Tier\n end")) "supporter") ;; ── guarded transitions: invalid moves are explicit errors ─────── (id-membership-test "requesting twice is an error" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n case identity_membership:request(M, alice, basic) of\n ok -> ok;\n {error, Why} -> Why\n end")) "exists") (id-membership-test "approving an unknown subject is not_found" (idmnm (idm-ev "M = identity_membership:start(),\n case identity_membership:approve(M, ghost) of\n ok -> ok;\n {error, Why} -> Why\n end")) "not_found") (id-membership-test "approving an already-active membership is an error" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n case identity_membership:approve(M, alice) of\n ok -> ok;\n {error, Why} -> Why\n end")) "active") ;; ── lapse / reinstate ──────────────────────────────────────────── (id-membership-test "active member can lapse" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n identity_membership:lapse(M, alice),\n case identity_membership:status(M, alice) of\n {ok, St, _} -> St\n end")) "lapsed") (id-membership-test "lapsing a pending membership is an error" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n case identity_membership:lapse(M, alice) of\n ok -> ok;\n {error, Why} -> Why\n end")) "pending") (id-membership-test "lapsed member can reinstate to active" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n identity_membership:lapse(M, alice),\n identity_membership:reinstate(M, alice),\n case identity_membership:status(M, alice) of\n {ok, St, _} -> St\n end")) "active") ;; ── revoke is terminal ─────────────────────────────────────────── (id-membership-test "any member can be revoked" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n identity_membership:revoke(M, alice),\n case identity_membership:status(M, alice) of\n {ok, St, _} -> St\n end")) "revoked") (id-membership-test "a revoked membership cannot be reinstated" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n identity_membership:revoke(M, alice),\n case identity_membership:reinstate(M, alice) of\n ok -> ok;\n {error, Why} -> Why\n end")) "revoked") ;; ── per-app grant projection ───────────────────────────────────── (id-membership-test "active member projects as member" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n case identity_membership:project(M, alice, blog) of\n {member, _, _} -> member;\n {Tag, _} -> Tag\n end")) "member") (id-membership-test "projection carries the requesting app" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n case identity_membership:project(M, alice, market) of\n {member, _, App} -> App\n end")) "market") (id-membership-test "the same subject projects consistently across apps" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, supporter),\n identity_membership:approve(M, alice),\n {member, T1, blog} = identity_membership:project(M, alice, blog),\n {member, T2, events} = identity_membership:project(M, alice, events),\n case T1 =:= T2 of\n true -> T1;\n false -> mismatch\n end")) "supporter") (id-membership-test "unknown subject projects as non_member" (idmnm (idm-ev "M = identity_membership:start(),\n case identity_membership:project(M, ghost, blog) of\n {Tag, _} -> Tag;\n {Tag, _, _} -> Tag\n end")) "non_member") (id-membership-test "lapsed member projects as lapsed" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n identity_membership:lapse(M, alice),\n case identity_membership:project(M, alice, blog) of\n {Tag, _} -> Tag;\n {Tag, _, _} -> Tag\n end")) "lapsed") (id-membership-test "revoked member projects as denied" (idmnm (idm-ev "M = identity_membership:start(),\n identity_membership:request(M, alice, basic),\n identity_membership:approve(M, alice),\n identity_membership:revoke(M, alice),\n case identity_membership:project(M, alice, blog) of\n {Tag, _} -> Tag;\n {Tag, _, _} -> Tag\n end")) "denied") (define id-membership-test-summary (str "membership " id-membership-test-pass "/" id-membership-test-count))