Skip to content

Commit 604ec43

Browse files
committed
test(testing): add unit tests for setupClerkTestingToken
1 parent 8dc633e commit 604ec43

4 files changed

Lines changed: 385 additions & 1 deletion

File tree

packages/testing/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,9 @@
7272
"dev:pub": "pnpm dev -- --env.publish",
7373
"format": "node ../../scripts/format-package.mjs",
7474
"format:check": "node ../../scripts/format-package.mjs --check",
75-
"lint": "eslint src"
75+
"lint": "eslint src",
76+
"test": "vitest run",
77+
"test:ci": "vitest run --maxWorkers=70%"
7678
},
7779
"dependencies": {
7880
"@clerk/backend": "workspace:^",
Lines changed: 357 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,357 @@
1+
import type { BrowserContext, Request, Route } from '@playwright/test';
2+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { ERROR_MISSING_FRONTEND_API_URL } from '../../common/errors';
5+
6+
// We need to reset the module-level WeakSet between tests
7+
let setupClerkTestingToken: typeof import('../setupClerkTestingToken').setupClerkTestingToken;
8+
9+
function createMockRoute(overrides: { url?: string; fetchStatus?: number; fetchJson?: unknown; fetchError?: Error } = {}) {
10+
const {
11+
url = 'https://clerk.example.com/v1/client',
12+
fetchStatus = 200,
13+
fetchJson = { response: { captcha_bypass: false } },
14+
fetchError,
15+
} = overrides;
16+
17+
const fulfilled: { response?: unknown; json?: unknown }[] = [];
18+
const continued: { url?: string }[] = [];
19+
let fetchCallCount = 0;
20+
21+
const route: Route = {
22+
request: () =>
23+
({
24+
url: () => url,
25+
}) as unknown as Request,
26+
fetch: vi.fn(async () => {
27+
fetchCallCount++;
28+
if (fetchError) {
29+
throw fetchError;
30+
}
31+
return {
32+
status: () => fetchStatus,
33+
json: async () => JSON.parse(JSON.stringify(fetchJson)),
34+
};
35+
}),
36+
fulfill: vi.fn(async (opts: any) => {
37+
fulfilled.push(opts);
38+
}),
39+
continue: vi.fn(async () => {}),
40+
} as unknown as Route;
41+
42+
return { route, fulfilled, continued, getFetchCallCount: () => fetchCallCount };
43+
}
44+
45+
function createMockContext() {
46+
let routeHandler: ((route: Route) => Promise<void>) | undefined;
47+
48+
const context = {
49+
route: vi.fn(async (_pattern: RegExp, handler: (route: Route) => Promise<void>) => {
50+
routeHandler = handler;
51+
}),
52+
} as unknown as BrowserContext;
53+
54+
return {
55+
context,
56+
getRouteHandler: () => routeHandler,
57+
getRouteCallCount: () => (context.route as ReturnType<typeof vi.fn>).mock.calls.length,
58+
};
59+
}
60+
61+
describe('setupClerkTestingToken', () => {
62+
const FAPI_URL = 'clerk.example.com';
63+
const TESTING_TOKEN = 'test_token_123';
64+
65+
beforeEach(async () => {
66+
vi.useFakeTimers();
67+
vi.stubEnv('CLERK_FAPI', FAPI_URL);
68+
vi.stubEnv('CLERK_TESTING_TOKEN', TESTING_TOKEN);
69+
70+
// Reset module to clear the WeakSet between tests
71+
vi.resetModules();
72+
const mod = await import('../setupClerkTestingToken');
73+
setupClerkTestingToken = mod.setupClerkTestingToken;
74+
});
75+
76+
afterEach(() => {
77+
vi.useRealTimers();
78+
vi.unstubAllEnvs();
79+
});
80+
81+
describe('validation', () => {
82+
it('throws when neither context nor page is provided', async () => {
83+
await expect(setupClerkTestingToken({} as any)).rejects.toThrow(
84+
'Either context or page must be provided to setup testing token',
85+
);
86+
});
87+
88+
it('throws when CLERK_FAPI is not set', async () => {
89+
vi.stubEnv('CLERK_FAPI', '');
90+
const { context } = createMockContext();
91+
await expect(setupClerkTestingToken({ context })).rejects.toThrow(ERROR_MISSING_FRONTEND_API_URL);
92+
});
93+
94+
it('uses frontendApiUrl option over env var', async () => {
95+
const { context, getRouteHandler } = createMockContext();
96+
await setupClerkTestingToken({ context, options: { frontendApiUrl: 'custom.clerk.com' } });
97+
98+
const handler = getRouteHandler();
99+
expect(handler).toBeDefined();
100+
101+
const { route, fulfilled } = createMockRoute({ url: 'https://custom.clerk.com/v1/client' });
102+
await handler!(route);
103+
104+
expect(route.fetch).toHaveBeenCalledWith({
105+
url: expect.stringContaining('custom.clerk.com'),
106+
});
107+
expect(fulfilled).toHaveLength(1);
108+
});
109+
});
110+
111+
describe('de-duplication', () => {
112+
it('registers route handler only once per context', async () => {
113+
const { context, getRouteCallCount } = createMockContext();
114+
115+
await setupClerkTestingToken({ context });
116+
await setupClerkTestingToken({ context });
117+
await setupClerkTestingToken({ context });
118+
119+
expect(getRouteCallCount()).toBe(1);
120+
});
121+
122+
it('registers separate handlers for different contexts', async () => {
123+
const ctx1 = createMockContext();
124+
const ctx2 = createMockContext();
125+
126+
await setupClerkTestingToken({ context: ctx1.context });
127+
await setupClerkTestingToken({ context: ctx2.context });
128+
129+
expect(ctx1.getRouteCallCount()).toBe(1);
130+
expect(ctx2.getRouteCallCount()).toBe(1);
131+
});
132+
133+
it('resolves context from page when context is not provided', async () => {
134+
const { context, getRouteCallCount } = createMockContext();
135+
const page = { context: () => context } as any;
136+
137+
await setupClerkTestingToken({ page });
138+
await setupClerkTestingToken({ page });
139+
140+
expect(getRouteCallCount()).toBe(1);
141+
});
142+
});
143+
144+
describe('route handler', () => {
145+
it('appends testing token to FAPI requests', async () => {
146+
const { context, getRouteHandler } = createMockContext();
147+
await setupClerkTestingToken({ context });
148+
149+
const { route } = createMockRoute();
150+
await getRouteHandler()!(route);
151+
152+
expect(route.fetch).toHaveBeenCalledWith({
153+
url: expect.stringContaining(`__clerk_testing_token=${TESTING_TOKEN}`),
154+
});
155+
});
156+
157+
it('overrides captcha_bypass in response', async () => {
158+
const { context, getRouteHandler } = createMockContext();
159+
await setupClerkTestingToken({ context });
160+
161+
const { route, fulfilled } = createMockRoute({
162+
fetchJson: { response: { captcha_bypass: false } },
163+
});
164+
await getRouteHandler()!(route);
165+
166+
expect(fulfilled).toHaveLength(1);
167+
expect(fulfilled[0].json.response.captcha_bypass).toBe(true);
168+
});
169+
170+
it('overrides captcha_bypass in piggybacking response', async () => {
171+
const { context, getRouteHandler } = createMockContext();
172+
await setupClerkTestingToken({ context });
173+
174+
const { route, fulfilled } = createMockRoute({
175+
fetchJson: { client: { captcha_bypass: false } },
176+
});
177+
await getRouteHandler()!(route);
178+
179+
expect(fulfilled).toHaveLength(1);
180+
expect(fulfilled[0].json.client.captcha_bypass).toBe(true);
181+
});
182+
183+
it('does not modify captcha_bypass when already true', async () => {
184+
const { context, getRouteHandler } = createMockContext();
185+
await setupClerkTestingToken({ context });
186+
187+
const { route, fulfilled } = createMockRoute({
188+
fetchJson: { response: { captcha_bypass: true } },
189+
});
190+
await getRouteHandler()!(route);
191+
192+
expect(fulfilled[0].json.response.captcha_bypass).toBe(true);
193+
});
194+
});
195+
196+
describe('retry on transient errors', () => {
197+
it('retries on 429 status', async () => {
198+
const { context, getRouteHandler } = createMockContext();
199+
await setupClerkTestingToken({ context });
200+
201+
let callCount = 0;
202+
const route = {
203+
request: () => ({ url: () => 'https://clerk.example.com/v1/client' }),
204+
fetch: vi.fn(async () => {
205+
callCount++;
206+
if (callCount <= 2) {
207+
return { status: () => 429, json: async () => ({}) };
208+
}
209+
return {
210+
status: () => 200,
211+
json: async () => ({ response: { captcha_bypass: false } }),
212+
};
213+
}),
214+
fulfill: vi.fn(),
215+
continue: vi.fn(),
216+
} as unknown as Route;
217+
218+
const handlerPromise = getRouteHandler()!(route);
219+
// Advance through retry delays
220+
await vi.advanceTimersByTimeAsync(60_000);
221+
await handlerPromise;
222+
223+
expect(callCount).toBe(3);
224+
expect(route.fulfill).toHaveBeenCalledTimes(1);
225+
});
226+
227+
it.each([502, 503, 504])('retries on %d status', async status => {
228+
const { context, getRouteHandler } = createMockContext();
229+
await setupClerkTestingToken({ context });
230+
231+
let callCount = 0;
232+
const route = {
233+
request: () => ({ url: () => 'https://clerk.example.com/v1/client' }),
234+
fetch: vi.fn(async () => {
235+
callCount++;
236+
if (callCount === 1) {
237+
return { status: () => status, json: async () => ({}) };
238+
}
239+
return {
240+
status: () => 200,
241+
json: async () => ({ response: { captcha_bypass: false } }),
242+
};
243+
}),
244+
fulfill: vi.fn(),
245+
continue: vi.fn(),
246+
} as unknown as Route;
247+
248+
const handlerPromise = getRouteHandler()!(route);
249+
await vi.advanceTimersByTimeAsync(60_000);
250+
await handlerPromise;
251+
252+
expect(callCount).toBe(2);
253+
expect(route.fulfill).toHaveBeenCalledTimes(1);
254+
});
255+
256+
it('does not retry on non-retryable status codes', async () => {
257+
const { context, getRouteHandler } = createMockContext();
258+
await setupClerkTestingToken({ context });
259+
260+
const { route, fulfilled, getFetchCallCount } = createMockRoute({ fetchStatus: 401 });
261+
await getRouteHandler()!(route);
262+
263+
expect(getFetchCallCount()).toBe(1);
264+
expect(fulfilled).toHaveLength(1);
265+
});
266+
267+
it('fulfills with raw response after exhausting retries on retryable status', async () => {
268+
const { context, getRouteHandler } = createMockContext();
269+
await setupClerkTestingToken({ context });
270+
271+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
272+
273+
const route = {
274+
request: () => ({ url: () => 'https://clerk.example.com/v1/client' }),
275+
fetch: vi.fn(async () => ({
276+
status: () => 429,
277+
json: async () => ({}),
278+
})),
279+
fulfill: vi.fn(),
280+
continue: vi.fn(),
281+
} as unknown as Route;
282+
283+
const handlerPromise = getRouteHandler()!(route);
284+
await vi.advanceTimersByTimeAsync(60_000);
285+
await handlerPromise;
286+
287+
// 1 initial + 3 retries = 4 total
288+
expect(route.fetch).toHaveBeenCalledTimes(4);
289+
expect(route.fulfill).toHaveBeenCalledTimes(1);
290+
expect(warnSpy).toHaveBeenCalledWith(expect.stringContaining('failed with status 429 after 4 attempts'));
291+
292+
warnSpy.mockRestore();
293+
});
294+
295+
it('retries on thrown errors and warns after exhausting retries', async () => {
296+
const { context, getRouteHandler } = createMockContext();
297+
await setupClerkTestingToken({ context });
298+
299+
const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
300+
const errorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
301+
302+
const networkError = new Error('net::ERR_CONNECTION_REFUSED');
303+
const route = {
304+
request: () => ({ url: () => 'https://clerk.example.com/v1/client' }),
305+
fetch: vi.fn(async () => {
306+
throw networkError;
307+
}),
308+
fulfill: vi.fn(async () => {}),
309+
continue: vi.fn(async () => {}),
310+
} as unknown as Route;
311+
312+
const handlerPromise = getRouteHandler()!(route);
313+
await vi.advanceTimersByTimeAsync(60_000);
314+
await handlerPromise;
315+
316+
expect(route.fetch).toHaveBeenCalledTimes(4);
317+
expect(route.continue).toHaveBeenCalledTimes(1);
318+
expect(warnSpy).toHaveBeenCalledWith(
319+
expect.stringContaining('failed after 4 attempts'),
320+
networkError,
321+
);
322+
323+
warnSpy.mockRestore();
324+
errorSpy.mockRestore();
325+
});
326+
327+
it('recovers after transient error on retry', async () => {
328+
const { context, getRouteHandler } = createMockContext();
329+
await setupClerkTestingToken({ context });
330+
331+
let callCount = 0;
332+
const route = {
333+
request: () => ({ url: () => 'https://clerk.example.com/v1/client' }),
334+
fetch: vi.fn(async () => {
335+
callCount++;
336+
if (callCount === 1) {
337+
throw new Error('network error');
338+
}
339+
return {
340+
status: () => 200,
341+
json: async () => ({ response: { captcha_bypass: false } }),
342+
};
343+
}),
344+
fulfill: vi.fn(),
345+
continue: vi.fn(),
346+
} as unknown as Route;
347+
348+
const handlerPromise = getRouteHandler()!(route);
349+
await vi.advanceTimersByTimeAsync(60_000);
350+
await handlerPromise;
351+
352+
expect(callCount).toBe(2);
353+
expect(route.fulfill).toHaveBeenCalledTimes(1);
354+
expect(route.continue).not.toHaveBeenCalled();
355+
});
356+
});
357+
});
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
"extends": "./tsconfig.json",
3+
"compilerOptions": {
4+
"sourceMap": true,
5+
"emitDeclarationOnly": false,
6+
"noImplicitAny": false
7+
},
8+
"include": ["src/**/*"]
9+
}

packages/testing/vitest.config.mts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import { defineConfig } from 'vitest/config';
2+
3+
export default defineConfig({
4+
test: {
5+
typecheck: {
6+
enabled: true,
7+
include: ['**/*.test.ts'],
8+
},
9+
coverage: {
10+
provider: 'v8',
11+
},
12+
fakeTimers: {
13+
toFake: ['setTimeout', 'clearTimeout'],
14+
},
15+
},
16+
});

0 commit comments

Comments
 (0)