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 testsTest matrix
| Layer | Provide | Assert |
|---|---|---|
| Primitives | In-memory stores, TestClock, deterministic crypto | Success and typed failures, expiry boundaries, continuation state, one-time consumption |
HTTP Operations / HttpApi | Substitute operation services, then build selected API groups | Inputs reach the operation once; status, error schema, headers, origin/CSRF policy, and cookies are correct |
| Browser client | Stub fetch and browser credential APIs | URLs and encoded bodies, passkey start/finish sequencing, typed API errors, disposal |
| Storage adapters | The real database engine and transaction mode | Round trips, ordering, Option semantics, rollback, atomic consume/claim/CAS |
| Concurrency and security | Parallel fibers or requests with shared real state | Exactly 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.