effect-auth

Testing

Test authentication services, operations, and HTTP APIs.

Authentication tests should become more realistic as they move outward, without making every assertion depend on a server or database. Keep time, randomness, storage, and network boundaries explicit so failures remain reproducible.

             / browser journeys \       few: WebAuthn, redirects, cookies
            / HTTP API + storage  \      schemas, middleware, transactions
           / HTTP Operations       \     application boundary and policy
          / primitives              \    many: deterministic service tests

Test matrix

LayerProvideAssert
PrimitivesIn-memory stores, TestClock, deterministic cryptoSuccess and typed failures, expiry boundaries, continuation state, one-time consumption
HTTP Operations / HttpApiSubstitute operation services, then build selected API groupsInputs reach the operation once; status, error schema, headers, origin/CSRF policy, and cookies are correct
Browser clientStub fetch and browser credential APIsURLs and encoded bodies, passkey start/finish sequencing, typed API errors, disposal
Storage adaptersThe real database engine and transaction modeRound trips, ordering, Option semantics, rollback, atomic consume/claim/CAS
Concurrency and securityParallel fibers or requests with shared real stateExactly one winner, replay rejection, rate-limit accounting, revocation and rotation behavior

Most feature behavior belongs in primitive tests. Add an Operations test when your application substitutes or wraps an exported *HttpOperations service. Add an HttpApi test for transport behavior: schema decoding, middleware, response mapping, and the complete Set-Cookie value. Reserve browser tests for behavior that only exists around fetch, WebAuthn conversion, redirects, or shared client state.

Deterministic services

@effect-auth/core/Testing publicly exports makeTestCrypto, CryptoTestLive, makeTestPasswordHasher, PasswordHasherTestLive, and CoreTestingLive. The merged layer also supplies no-op audit logging and WaitUntil, plus test privacy services. These implementations are predictable, not secure; never provide them in production.

import { expect, it } from "@effect/vitest";
import { Crypto } from "@effect-auth/core/Crypto";
import { CryptoTestLive } from "@effect-auth/core/Testing";
import { Effect } from "effect";

it.effect("issues reproducible tokens", () =>
  Effect.gen(function* () {
    const crypto = yield* Crypto;
    expect(yield* crypto.randomToken(16)).toBe("test-token-16-1");
    expect(yield* crypto.randomToken(16)).toBe("test-token-16-2");
  }).pipe(Effect.provide(CryptoTestLive))
);

Use a fresh makeTestCrypto() when a test needs an isolated counter. Use CoreTestingLive when the subject requires several core collaborators, then provide feature stores and policies separately. Control deadlines with Effect's TestClock; set an exact epoch and test immediately before, at, and after expiry rather than sleeping.

HTTP and browser boundaries

For an Operations test, construct the public service with PasswordHttpOperations.of(...) (or the relevant feature), provide it with Layer.succeed, and build the exported API group. This proves a built-in or Custom API binds your service without requiring real credentials or persistence.

At the API layer, assert the wire contract rather than only the decoded success value. Cover malformed payloads and the stable error body described by the Error Model. For session, refresh, approval, and trusted-device flows, carry request cookies between calls and assert cookie name, path, HttpOnly, Secure, SameSite, expiry, and clearing behavior. Continuation flows must reject a missing, wrong-device, expired, or already-consumed continuation.

Browser client tests can inject fetch through createAuthClient. Return real Response objects, record each Request, and stub CredentialsContainer.create/get for passkeys. Check base64url conversion and start/finish order, then call client.dispose(). Keep one small real-server journey to catch header handling that a fetch stub cannot model.

Storage and adversarial cases

Run adapter tests against the production engine, not an in-memory imitation. In addition to ordinary CRUD, launch concurrent attempts against verification codes, recovery codes, OAuth grants, refresh rotation, passkey counters, and webhook claims. Assert exactly the contractually allowed number succeed and that losers do not mutate state. Rate-limit tests should verify the first allowed decision, the denied decision, window reset under controlled time, key isolation, and that middleware does not consume budget twice.

The repository has broad internal runners in packages/core/test/StorageContract.ts, but they are not public exports. Do not import repository test files or dist internals. Treat those cases as an executable specification and reproduce the relevant semantics in your adapter suite. See Custom Database for atomicity, exact expiry boundaries, and the full adapter checklist.

On this page