first commit
This commit is contained in:
101
docs/api-stack/api/api-auth/src/testing/conformance.md
Normal file
101
docs/api-stack/api/api-auth/src/testing/conformance.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# `testing` — `runAuthSessionConformance` harness
|
||||
|
||||
<!-- begin:auto-docs -->
|
||||
_(no submodules)_
|
||||
<!-- end:auto-docs -->
|
||||
|
||||
## Purpose
|
||||
|
||||
A reusable Vitest `describe(...)` block every concrete `SessionResolver` must
|
||||
pass before publishing. The harness covers the three behaviours that define a
|
||||
correct resolver implementation independent of the underlying credential
|
||||
shape: returning `null` for unrecognised requests, returning the documented
|
||||
`Principal` for a recognised request, and an idempotent `dispose()`.
|
||||
|
||||
Concrete adapters in Stages 4/6/8 (`api-auth-jwt`, `api-auth-anonymous`,
|
||||
`api-auth-bearer-static`, `api-auth-better-auth`, `api-auth-rolled`) call
|
||||
`runAuthSessionConformance({ … })` from one of their `*.test.ts` files. The
|
||||
single conformance suite means every adapter ships against the same observable
|
||||
contract; new adapters drop in without re-deriving the test plan.
|
||||
|
||||
## Features
|
||||
|
||||
- Pure function — wires `describe` / `it` from `@suaveplan/testing/runner` so
|
||||
the suite runs identically under Vitest (default) and `bun:test` (under the
|
||||
`bun` test condition).
|
||||
- Factory pattern — adapters re-construct their resolver inside each `it` so
|
||||
parallel runners do not share resolver state across tests.
|
||||
- Every `it` carries `{ timeout: 5000 }` per the project's hard rule.
|
||||
|
||||
## Usage: Basic
|
||||
|
||||
```ts
|
||||
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
|
||||
import type { PrincipalId, Scope } from "@suaveplan/api-types/principal";
|
||||
|
||||
import { buildAnonymousResolver } from "../src/anonymous.js";
|
||||
|
||||
runAuthSessionConformance({
|
||||
label: "anonymous",
|
||||
factory: () => buildAnonymousResolver(),
|
||||
seedNull: () => new Request("https://example.test/api"),
|
||||
seedPrincipal: () => ({
|
||||
request: new Request("https://example.test/api", {
|
||||
headers: { "x-test-principal": "1" },
|
||||
}),
|
||||
expected: {
|
||||
id: "anon" as PrincipalId,
|
||||
scopes: [] as readonly Scope[],
|
||||
claims: {},
|
||||
},
|
||||
}),
|
||||
});
|
||||
```
|
||||
|
||||
## Usage: Advanced
|
||||
|
||||
Compose the harness with adapter-specific extra coverage (per-adapter codes,
|
||||
JWK rotation, etc.):
|
||||
|
||||
```ts
|
||||
import { describe, it, expect } from "@suaveplan/testing/runner";
|
||||
import { runAuthSessionConformance } from "@suaveplan/api-auth/testing";
|
||||
|
||||
import { buildJwtResolver, JWT_CODES } from "../src/index.js";
|
||||
|
||||
runAuthSessionConformance({
|
||||
label: "jwt-hs256",
|
||||
factory: async () => buildJwtResolver({ jwks: testJwks }),
|
||||
seedNull: () => new Request("https://example.test/api"),
|
||||
seedPrincipal: () => ({
|
||||
request: new Request("https://example.test/api", {
|
||||
headers: { authorization: `Bearer ${testToken}` },
|
||||
}),
|
||||
expected: testPrincipal,
|
||||
}),
|
||||
});
|
||||
|
||||
describe("jwt-hs256 specific", () => {
|
||||
it("emits JWT.EXPIRED on stale token", { timeout: 5000 }, async () => {
|
||||
/* … */
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
## API Reference
|
||||
|
||||
| Symbol | Kind | Description |
|
||||
|-----------------------------------|-------------|----------------------------------------------------------------------|
|
||||
| `runAuthSessionConformance` | function | Adds a `describe(...)` block; runs three baseline scenarios. |
|
||||
| `AuthSessionConformanceFactory` | interface | `{ factory, label, seedNull, seedPrincipal }`. |
|
||||
|
||||
## Implementation Notes
|
||||
|
||||
- The harness asserts `id` + `scopes` equality on the principal, not deep
|
||||
equality of `claims` — `claims` is intentionally free-form and adapters may
|
||||
add provider-specific fields that callers must not pin in conformance.
|
||||
- `dispose()` is asserted to resolve to `undefined`, not just "not throw" —
|
||||
some legacy resolvers were returning the wrong shape from the second
|
||||
invocation; this assertion caught it.
|
||||
- `expect` and `describe`/`it` come from `@suaveplan/testing/runner` per
|
||||
project rule 11.
|
||||
Reference in New Issue
Block a user