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.
| Layer | Provide | Assert |
|---|---|---|
| Domain services | Test stores, TestClock, deterministic crypto | Typed outcomes, expiry boundaries, one-time use, continuations |
| HTTP Operations | Substitute *HttpOperations services | App policy and wrapper behavior |
HttpApi | Selected groups, middleware, real Request/Response | Decoding, status, public errors, headers, cookies, origin policy |
| Browser client | Stub fetch and credential APIs | URL/body encoding, typed failures, passkey sequence, cancellation |
| Storage | Production engine and transaction mode | Round trips, ordering, rollback, atomic consume/claim/CAS |
| Browser journey | Deployed-shaped Cloudflare topology | Cookies, 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:
| Environment | Proves | Does not prove |
|---|---|---|
| Fake D1 unit test | Query translation and contract logic | D1 service behavior |
| Local Alchemy/workerd | Bindings and deployed-shaped request flow | Production-region D1 behavior |
| Deployed D1 smoke | Real platform integration | Full 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.