Files
skills/docs/api-stack/api/api-auth/src/testing/conformance.md
2026-05-29 08:20:56 +01:00

3.8 KiB

testing — runAuthSessionConformance harness

(no submodules)

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

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.):

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.