;; identity/tests/expiry.sx — access-token expiry on a logical clock ;; (RFC 6749 §4.2.2 expires_in). `advance` stands in for time passing; ;; introspect returns inactive once the clock reaches a token's expiry. ;; Refresh mints a fresh short-lived access token — the point of refresh. (define id-expiry-test-count 0) (define id-expiry-test-pass 0) (define id-expiry-test-fails (list)) (define id-expiry-test (fn (name actual expected) (set! id-expiry-test-count (+ id-expiry-test-count 1)) (if (= actual expected) (set! id-expiry-test-pass (+ id-expiry-test-pass 1)) (append! id-expiry-test-fails {:name name :expected expected :actual actual})))) (define ide-ev erlang-eval-ast) (define idenm (fn (v) (get v :name))) (identity-load-token!) ;; ── within TTL is active; past TTL is inactive ─────────────────── (id-expiry-test "a token within its TTL is active" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 50),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "active") (id-expiry-test "a token at its TTL boundary is expired" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 100),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "inactive") (id-expiry-test "a token just before its TTL is still active" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 99),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "active") ;; ── no TTL (infinity) never expires ────────────────────────────── (id-expiry-test "a token issued without a TTL never expires" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read),\n identity_tokens:advance(R, 100000),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "active") ;; ── refresh mints a fresh short-lived token ────────────────────── (id-expiry-test "refresh renews access after the old token expired" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, A, Rt} = identity_tokens:issue_grant(R, alice, web, read, 100),\n identity_tokens:advance(R, 100),\n inactive = case identity_tokens:introspect(R, A) of\n {active, _, _, _} -> active; {inactive} -> inactive end,\n {ok, A2, _R2} = identity_tokens:refresh(R, Rt),\n case identity_tokens:introspect(R, A2) of\n {active, _, _, _} -> renewed;\n {inactive} -> inactive\n end")) "renewed") (id-expiry-test "the renewed token also expires after its own TTL" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, _A, Rt} = identity_tokens:issue_grant(R, alice, web, read, 100),\n identity_tokens:advance(R, 100),\n {ok, A2, _R2} = identity_tokens:refresh(R, Rt),\n identity_tokens:advance(R, 100),\n case identity_tokens:introspect(R, A2) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "inactive") ;; ── the logical clock ──────────────────────────────────────────── (id-expiry-test "the clock starts at zero and advances" (ide-ev "R = identity_tokens:start(),\n identity_tokens:advance(R, 7),\n identity_tokens:advance(R, 35),\n identity_tokens:now(R)") 42) ;; ── expiry composes with revocation ────────────────────────────── (id-expiry-test "an expired token is also inactive after revoke (no contradiction)" (idenm (ide-ev "R = identity_tokens:start(),\n {ok, T} = identity_tokens:issue(R, alice, web, read, 100),\n identity_tokens:advance(R, 200),\n identity_tokens:revoke(R, T),\n case identity_tokens:introspect(R, T) of\n {active, _, _, _} -> active;\n {inactive} -> inactive\n end")) "inactive") (define id-expiry-test-summary (str "expiry " id-expiry-test-pass "/" id-expiry-test-count))