# Testing

Grantor is built TDD, chunk by chunk (see `.superpowers/sdd/progress.md` for
the full ledger): a failing test is written first, then the minimal code to
pass it, then a subagent-driven review pass before moving on. This page
describes the resulting test strategy and how to run it.

## Layers

### 1. Contract (`contracts/`, Foundry)

`contracts/test/GrantorRegistry.t.sol` — unit tests covering deployment
immutables, tenant creation, multiwallet admin (add/remove, non-admin
rejection), tier configuration (owner-only), billing (`topUp`, `drawPeriod`
charging + activating, rejecting a draw while still active, rejecting on
insufficient balance), the derived `status()` state machine (active → grace →
inactive), `withdrawBalance` (admin-only, returns unused funds),
`registerIssuerKey`/`isActive` (including "requires active tenant" and
grace-vs-inactive behavior), `deregisterIssuerKey` (stops trust), app/agent
registration counters, `pause` blocking `createTenant`, `setGraceWindow`
(owner-only), the fee-snapshot anti-drain guarantee
(`test_owner_cannot_drain_tenant_via_fee_raise`), and the `autoRenew` gate
(on by default; turning it off blocks `drawPeriod` and allows a safe
withdrawal).

`contracts/test/GrantorRegistry.invariant.t.sol` — a Foundry invariant test.
A `Handler` contract drives two independent tenants through randomized
`topUp`/`draw` (warps 31 days, tolerates reverts)/`withdraw` sequences; the
invariant asserted after every run is `usdc.balanceOf(registry) == sum of all
tenants' balances` — this is where money-handling correctness lives, so it
gets the heaviest fuzz budget of any component in the repo.

Run:

```bash
cd contracts
forge test
```

### 2. `grantor-verify` (Rust)

Unit tests in `crates/grantor-verify/src/lib.rs`: ES256 JWT verification
(`verify_jwt`), the RFC 7638 JWK thumbprint (`issuer_key_id`) that must equal
the on-chain `keyId`, and the `TrustSource` trait + `verify()` composition
(token cryptographically valid *and* the trust source reports the key
active).

### 3. `grantor-issuer` (Rust, unit + Poem endpoint tests)

Unit tests live alongside the code they test, `#[cfg(test)]`-gated in the same
file:

- `token.rs` — JWT minting round-trips through `grantor_verify::verify_jwt`,
  `key_id` matches the verify crate's own thumbprint computation, JWKS is
  well-formed and carries the right `kid`, `pairwise_sub` is per-salt and
  case-insensitive, PEM loading rejects junk and accepts a real key.
- `login.rs` — EIP-191 recovery: recovers the true signer, rejects a wrong
  expected address, and a tampered message recovers to a different address
  than the real signer.
- `cache.rs` — the four `CachedIsActive` availability cases (fresh hit, TTL
  refresh, serve-stale-within-bound, fail-closed past the bound) plus a
  cold-failure case, using a fake `IsActiveSource` and an injectable `Clock`
  so no anvil is needed.
- `gate.rs` — `ChainGate` fails closed when the RPC is unreachable and the
  cache is cold.
- `config.rs` — env parsing (`IssuerConfig::from_map`) succeeds on a full env
  and errors on a missing required var; a dedicated assertion that `Debug`
  redacts `signing_key_pem`/`tenant_salt`.
- `clients.rs` — exact-match redirect/audience lookups (including the
  trailing-slash / different-path / unknown-client negative cases), TOML
  parsing of a full registry, a parse-error path, and an empty registry
  rejecting everything (fail-closed default).
- `state.rs` — single-use + expiring auth codes, single-use + expiring
  nonces, and that `AppState::for_test()` seeds a usable client store.
- `http/discovery.rs`, `http/page.rs` — OIDC discovery document shape and the
  connect-and-sign HTML page (including an HTML-escaping regression test for
  the reflected query parameters — a CRITICAL finding from an earlier review,
  fixed by escaping before interpolation).
- `http/authorize.rs` — Grant A over a Poem `TestClient`: valid SIWE issues a
  code and 302s; PKCE is required with S256 only; bad SIWE signature, domain
  mismatch, and nonce replay are all rejected; unregistered client/redirect is
  rejected at both `GET /authorize` and `POST /authorize/callback` with **no
  `Location` header** on failure; an inactive tenant gets 403.
- `http/token.rs`, `http/token_code.rs`, `http/assertion.rs` — Grant C over a
  `TestClient`: valid assertion mints for a registered, active agent key;
  inactive tenant → 403; non-agent key → 400; unregistered audience → 400.
  Grant A code exchange: full SIWE→code→`/token`→verify round trip, single-use
  code enforcement, and a wrong-PKCE-verifier → 400 case.
- `http/mod.rs` — router wiring (`/healthz`, discovery + jwks mount, the
  `/assets` static endpoint serving the login script with a JS content type).

Poem's `TestClient` (`poem::test::TestClient`, gated behind the `test`
feature in `[dev-dependencies]`) drives these as real HTTP requests against
the router, not direct function calls — so route wiring, form/query
extraction, and status codes are exercised, not just the handler logic.

### 4. Live-anvil end-to-end tests

`crates/grantor-issuer/tests/grant_a_e2e.rs` and
`crates/grantor-issuer/tests/grant_c_e2e.rs` are the two full-stack tests:
each spawns a real `anvil` node, deploys `MockUSDC` + `GrantorRegistry` from
their Foundry build artifacts (`contracts/out/*/*.json`, referenced via
`alloy::sol!`), runs a full tenant lifecycle (`setTierConfig` → `createTenant`
→ `topUp` → `drawPeriod`), registers the issuer key's RFC 7638 thumbprint
on-chain, wires up the real `AppState`/`ChainGate`/`FileClientStore` pointed
at the anvil endpoint, and drives the actual HTTP router:

- `grant_a_e2e.rs` — a user wallet signs a SIWE message, `POST
  /authorize/callback` → 302 with a code, `POST /token` with the PKCE
  verifier → JWT, then `grantor_verify::verify_jwt` against the issuer's real
  public key, asserting the OIDC `nonce` echoes, `tid` is correct, and `sub` is
  pairwise (never equal to the raw wallet address).
- `grant_c_e2e.rs` — additionally registers an on-chain agent key, signs the
  canonical client-credentials assertion with it, `POST /token` with
  `grant_type=client_credentials` → JWT, then verifies it the same way.

These are genuine end-to-end tests against a live chain, not a mock: an
earlier review confirmed removing the on-chain registration step turns the
happy path into a 403, so the test is not vacuous.

## Running everything

```bash
# workspace unit + Poem endpoint tests + both live-anvil e2e tests
# (anvil must be on PATH — see the env note below)
cargo test --workspace

# contract unit + invariant/fuzz tests
cd contracts && forge test

# lint (must be clean before a review pass)
cargo clippy --workspace --all-targets
```

Environment needed for the Rust side (Foundry's `anvil`/`forge`/`cast` and
Cargo's build output are not on the default PATH/location on this box):

```bash
export PATH="$HOME/.foundry/bin:$HOME/.cargo/bin:$PATH"
export CARGO_TARGET_DIR=~/.cargo-target
```

Without `anvil` on `PATH`, the two `tests/grant_*_e2e.rs` tests fail fast with
an explicit "anvil must be installed" panic rather than a confusing timeout.

## Process

Implementation followed the plans under `docs/superpowers/plans/` chunk by
chunk: write a failing test for the next bite-sized behavior, implement just
enough to pass it, then hand the diff to a subagent-driven review pass before
moving to the next chunk (see `.superpowers/sdd/progress.md` for the
per-task review outcomes — most chunks were "Approved, no issues," a few
surfaced Important/Critical findings that were fixed before merge, e.g. the
reflected-XSS fix in `http/page.rs` and the signature-before-nonce-burn
ordering fix in both `http/assertion.rs` and `http/authorize.rs`). A final
whole-branch review preceded each merge-readiness verdict.
