effect-auth

Testing

Test authentication services, operations, HTTP boundaries, and storage.

Keep time, randomness, storage, and network explicit. Most cases belong in deterministic domain-service tests; add realism only where the boundary under test requires it.

Authentication tests become fewer and more realistic toward the browser boundary
LayerProvideAssert
Domain servicesTest stores, TestClock, deterministic cryptoTyped outcomes, expiry boundaries, one-time use, continuations
HTTP OperationsSubstitute *HttpOperations servicesApp policy and wrapper behavior
HttpApiSelected groups, middleware, real Request/ResponseDecoding, status, public errors, headers, cookies, origin policy
Browser clientStub fetch and credential APIsURL/body encoding, typed failures, passkey sequence, cancellation
StorageProduction engine and transaction modeRound trips, ordering, rollback, atomic consume/claim/CAS
Browser journeyDeployed-shaped Cloudflare topologyCookies, redirects, WebAuthn, service bindings

Deterministic collaborators

@effect-auth/core/Testing exports makeTestCrypto, CryptoTestLive, makeTestPasswordHasher, PasswordHasherTestLive, and CoreTestingLive. CoreTestingLive also provides no-op audit logging and WaitUntil, test privacy, and a test TOTP secret cipher. These layers are predictable, not secure.

Create a fresh crypto service when a test depends on its initial counter; the exported CryptoTestLive contains a shared service value.

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

it.effect("issues a reproducible invitation token", () => {
  const CryptoLive = Layer.succeed(Crypto)(makeTestCrypto());

  return Effect.gen(function* () {
    const crypto = yield* Crypto;
    expect(yield* crypto.randomToken(24)).toBe("test-token-24-1");
  }).pipe(Effect.provide(CryptoLive));
});

Import TestClock from effect/testing. Set an exact epoch and assert immediately before, at, and after expiry; never sleep in a unit test.

Operation and HTTP boundaries

Construct public operation substitutes with PasswordHttpOperations.of(...) or the relevant service, provide them through Layer.succeed, then build only the API group under test. This verifies that an app-owned binding invokes the operation once without requiring real credentials.

At the HTTP layer, assert the wire contract: malformed payloads, stable public error body, status, cache policy, origin/CSRF behavior, and complete Set-Cookie. For session and continuation flows, carry cookies between requests and reject missing, wrong-device, expired, or already-consumed state.

const response = await handler(
  new Request("https://app.example.com/auth/session", {
    headers: { cookie: sessionCookie, origin: "https://app.example.com" },
  })
);

expect(response.status).toBe(200);
expect(response.headers.get("set-cookie")).toContain("HttpOnly");

Browser client tests inject fetch through createAuthClient. Return real Response objects, record requests, and stub CredentialsContainer.create/get for passkeys. Keep one real-server journey for browser cookie and header behavior that a fetch stub cannot model.

Storage and concurrency

Run adapter tests against the production database engine, not an in-memory imitation. Race verification codes, recovery codes, OAuth grants, refresh rotation, passkey counters, and session assurance; assert exactly the permitted winners and no loser mutation. Test rate-limit first allow, denial, controlled window reset, key isolation, and single accounting through middleware.

For Cloudflare, distinguish three confidence levels:

EnvironmentProvesDoes not prove
Fake D1 unit testQuery translation and contract logicD1 service behavior
Local Alchemy/workerdBindings and deployed-shaped request flowProduction-region D1 behavior
Deployed D1 smokeReal platform integrationFull concurrency coverage by itself

The repository's D1 adapter suites use fake D1, and the split example smoke runs locally through Alchemy/workerd. Add a deployed, non-production Cloudflare smoke gate for production D1 confidence.

Internal runners in packages/core/test/StorageContract.ts are not package exports. Do not import repository test files or dist internals; reproduce the relevant semantics in your adapter suite. See Custom Database for exact boundaries and the adapter checklist.

On this page