Bridge Training
Auth

Testing

Playwright helpers, the Clerk test-user pool and factories for webhook tests.

Playwright: @workspace/auth/testing/playwright

Wraps @clerk/testing so e2e suites never import it. Requires CLERK_PUBLISHABLE_KEY and CLERK_SECRET_KEY in the e2e env.

authSetup

Enables Clerk's testing mode. Call it once in the global setup project.

import { authSetup, ensureTestUserPool } from '@workspace/auth/testing/playwright';

setup('provision clerk pool', async () => {
  await authSetup();
  await ensureTestUserPool({
    secretKey: process.env.CLERK_SECRET_KEY,
    size: E2E_WORKER_POOL_SIZE,
    emailPattern: (i) => `teacher-e2e-w${i}@example.com`,
    password: teacherPassword(),
    signupSource: 'app',
  });
});

ensureTestUserPool

Makes sure one Clerk user exists per worker, creating the missing ones with publicMetadata.e2e = true and the pool's signupSource, so GET /accounts/me can create their account just in time after a database wipe. Idempotent: an existing e-mail is reused, and its public signupSource is set only when missing or different.

Prop

Type

Returns PooledTestUser[] with email, userId and workerIndex.

setupAuthTestingToken

Injects Clerk's testing token into a page so bot protection does not block the sign-in form. Call it before each navigation that reaches an auth screen.

import { setupAuthTestingToken } from '@workspace/auth/testing/playwright';

const page = await ctx.newPage();
await setupAuthTestingToken({ page });
await page.goto('/signin');

The app's e2e suite signs in once per worker through the real form, saves the storage state to a file, and reuses it for every test of that worker.

clerk

The programmatic helpers of @clerk/testing: clerk.signIn, clerk.signOut, clerk.loaded. Prefer the form for the sign-in path under test, and these for a quick identity switch.

Webhook factories: @workspace/auth/server/testing

Faker factories for the Clerk adapter, kept here so the "clerk" literal and Clerk-shaped payloads never leak into the domain factories. Every function takes faker first, then overrides.

FactoryBuilds
makeClerkIngestedEvent(faker, overrides?)An IngestedEvent<"clerk">, eventType defaults to user.created
makeClerkUserJSON(faker, { email?, ...overrides })A user.created / user.updated payload with the fields the adapter reads
makeClerkDeletedObjectJSON(faker, overrides?)A user.deleted payload, deleted: true
makeClerkWebhookSecret(faker)A whsec_ signing secret
signClerkWebhookRequest(faker, input)A Request signed the way Clerk (svix) signs it

The signed request goes through the real verifier, so an integration test covers the signature path end to end:

import {
  makeClerkUserJSON,
  makeClerkWebhookSecret,
  signClerkWebhookRequest,
} from '@workspace/auth/server/testing';
import { createClerkWebhookVerifier } from '@workspace/auth/server';

const secret = makeClerkWebhookSecret(faker);
const verifier = createClerkWebhookVerifier(secret);

const request = signClerkWebhookRequest(faker, {
  type: 'user.updated',
  data: makeClerkUserJSON(faker, { email: 'teacher@example.com' }),
  secret,
});

const event = await verifier.verify(request);

signClerkWebhookRequest also takes deliveryId to replay a delivery, signedAt to test the timestamp tolerance, and rawBody to sign an arbitrary body.

Unit tests of consumers

  • A component that only reads an AuthPort takes a hand-written object, see Hooks and types.
  • A component that calls the hooks renders inside a simulated provider.
  • A server test of the API's identity middleware stubs verifyToken with a function returning a TokenPayload; nothing in the package needs mocking.