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.
| Factory | Builds |
|---|---|
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
AuthPorttakes 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
verifyTokenwith a function returning aTokenPayload; nothing in the package needs mocking.