From bdb01fd86763bbf5401a0b561331337106ea3f20 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 12 Mar 2026 16:02:03 -0400 Subject: [PATCH 1/3] test(integration): add e2e test for getToken with custom JWT templates and skipCache --- .changeset/dull-breads-watch.md | 2 + .../tests/debug-and-custom-jwt.test.ts | 116 ++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 .changeset/dull-breads-watch.md create mode 100644 integration/tests/debug-and-custom-jwt.test.ts diff --git a/.changeset/dull-breads-watch.md b/.changeset/dull-breads-watch.md new file mode 100644 index 00000000000..a845151cc84 --- /dev/null +++ b/.changeset/dull-breads-watch.md @@ -0,0 +1,2 @@ +--- +--- diff --git a/integration/tests/debug-and-custom-jwt.test.ts b/integration/tests/debug-and-custom-jwt.test.ts new file mode 100644 index 00000000000..f91d4a1cf56 --- /dev/null +++ b/integration/tests/debug-and-custom-jwt.test.ts @@ -0,0 +1,116 @@ +import { expect, test } from '@playwright/test'; + +import type { Application } from '../models/application'; +import { appConfigs } from '../presets'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils } from '../testUtils'; + +test.describe('Custom JWT templates with skipCache @nextjs', () => { + test.describe.configure({ mode: 'serial' }); + + let app: Application; + let fakeUser: FakeUser; + let jwtTemplateId: string; + const jwtTemplateName = `e2e-test-${Date.now()}`; + + test.beforeAll(async () => { + test.setTimeout(120_000); + + app = await appConfigs.next.appRouter + .clone() + .addFile( + 'src/middleware.ts', + () => `import { clerkMiddleware } from '@clerk/nextjs/server'; + +export default clerkMiddleware(); + +export const config = { + matcher: ['/((?!.*\\\\..*|_next).*)', '/', '/(api|trpc)(.*)'], +}; +`, + ) + .addFile( + 'src/app/api/custom-jwt/route.ts', + () => `import { headers } from 'next/headers'; +import { auth } from '@clerk/nextjs/server'; + +export async function GET() { + const headersList = await headers(); + const templateName = headersList.get('x-jwt-template'); + const skipCache = headersList.get('x-skip-cache') === 'true'; + const { getToken, userId, sessionId } = await auth(); + const customToken = await getToken({ template: templateName, skipCache }); + return Response.json({ + userId, + sessionId, + customToken, + }); +}`, + ) + .commit(); + + await app.setup(); + await app.withEnv(appConfigs.envs.withEmailCodes); + await app.dev(); + + const m = createTestUtils({ app }); + fakeUser = m.services.users.createFakeUser(); + await m.services.users.createBapiUser(fakeUser); + + const template = await m.services.clerk.jwtTemplates.create({ + name: jwtTemplateName, + claims: { test_claim: 'hello_from_e2e' }, + lifetime: 60, + }); + jwtTemplateId = template.id; + }); + + test.afterAll(async () => { + const m = createTestUtils({ app }); + if (jwtTemplateId) { + await m.services.clerk.jwtTemplates.delete(jwtTemplateId); + } + await fakeUser.deleteIfExists(); + await app.teardown(); + }); + + test('getToken with skipCache returns a fresh custom JWT token on each call', async ({ page, context }) => { + const u = createTestUtils({ app, page, context }); + + await u.po.signIn.goTo(); + await u.po.signIn.waitForMounted(); + await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); + await u.po.expect.toBeSignedIn(); + + const fetchToken = (skipCache: boolean) => + page.request.get(`${app.serverUrl}/api/custom-jwt`, { + headers: { + 'x-jwt-template': jwtTemplateName, + 'x-skip-cache': String(skipCache), + }, + }); + + // Without skipCache + const res1 = await fetchToken(false); + expect(res1.status()).toBe(200); + const body1 = await res1.json(); + expect(body1.userId).toBeTruthy(); + expect(body1.sessionId).toBeTruthy(); + expect(body1.customToken).toBeTruthy(); + + const payload1 = JSON.parse(atob(body1.customToken.split('.')[1])); + expect(payload1.test_claim).toBe('hello_from_e2e'); + + // With skipCache — should return a valid, freshly issued token + const res2 = await fetchToken(true); + expect(res2.status()).toBe(200); + const body2 = await res2.json(); + expect(body2.userId).toBeTruthy(); + expect(body2.sessionId).toBeTruthy(); + expect(body2.customToken).toBeTruthy(); + + const payload2 = JSON.parse(atob(body2.customToken.split('.')[1])); + expect(payload2.test_claim).toBe('hello_from_e2e'); + expect(payload2.iat).toBeGreaterThanOrEqual(payload1.iat); + }); +}); From a017154fd0d31a91ae9d6739d9ad83acd9d080fa Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 12 Mar 2026 18:01:51 -0400 Subject: [PATCH 2/3] chore: Mod tests as getToken w/ custom template always skips cache --- .../tests/debug-and-custom-jwt.test.ts | 25 ++++++++----------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/integration/tests/debug-and-custom-jwt.test.ts b/integration/tests/debug-and-custom-jwt.test.ts index f91d4a1cf56..219f9b53365 100644 --- a/integration/tests/debug-and-custom-jwt.test.ts +++ b/integration/tests/debug-and-custom-jwt.test.ts @@ -5,7 +5,7 @@ import { appConfigs } from '../presets'; import type { FakeUser } from '../testUtils'; import { createTestUtils } from '../testUtils'; -test.describe('Custom JWT templates with skipCache @nextjs', () => { +test.describe('Custom JWT templates @nextjs', () => { test.describe.configure({ mode: 'serial' }); let app: Application; @@ -37,9 +37,9 @@ import { auth } from '@clerk/nextjs/server'; export async function GET() { const headersList = await headers(); const templateName = headersList.get('x-jwt-template'); - const skipCache = headersList.get('x-skip-cache') === 'true'; const { getToken, userId, sessionId } = await auth(); - const customToken = await getToken({ template: templateName, skipCache }); + // Always returns a valid, freshly issued token + const customToken = await getToken({ template: '${jwtTemplateName}' }); return Response.json({ userId, sessionId, @@ -82,16 +82,9 @@ export async function GET() { await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password }); await u.po.expect.toBeSignedIn(); - const fetchToken = (skipCache: boolean) => - page.request.get(`${app.serverUrl}/api/custom-jwt`, { - headers: { - 'x-jwt-template': jwtTemplateName, - 'x-skip-cache': String(skipCache), - }, - }); + const fetchToken = () => page.request.get(`${app.serverUrl}/api/custom-jwt`); - // Without skipCache - const res1 = await fetchToken(false); + const res1 = await fetchToken(); expect(res1.status()).toBe(200); const body1 = await res1.json(); expect(body1.userId).toBeTruthy(); @@ -101,8 +94,10 @@ export async function GET() { const payload1 = JSON.parse(atob(body1.customToken.split('.')[1])); expect(payload1.test_claim).toBe('hello_from_e2e'); - // With skipCache — should return a valid, freshly issued token - const res2 = await fetchToken(true); + // Wait >1s so the next token gets a different `iat` (seconds granularity) + await page.waitForTimeout(1500); + + const res2 = await fetchToken(); expect(res2.status()).toBe(200); const body2 = await res2.json(); expect(body2.userId).toBeTruthy(); @@ -111,6 +106,6 @@ export async function GET() { const payload2 = JSON.parse(atob(body2.customToken.split('.')[1])); expect(payload2.test_claim).toBe('hello_from_e2e'); - expect(payload2.iat).toBeGreaterThanOrEqual(payload1.iat); + expect(payload2.iat).toBeGreaterThan(payload1.iat); }); }); From 1a5cf77ed030c423fda43a53e040a437996317e4 Mon Sep 17 00:00:00 2001 From: Tom Milewski Date: Thu, 12 Mar 2026 18:08:25 -0400 Subject: [PATCH 3/3] chore: Rename file --- .../tests/{debug-and-custom-jwt.test.ts => custom-jwt.test.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename integration/tests/{debug-and-custom-jwt.test.ts => custom-jwt.test.ts} (100%) diff --git a/integration/tests/debug-and-custom-jwt.test.ts b/integration/tests/custom-jwt.test.ts similarity index 100% rename from integration/tests/debug-and-custom-jwt.test.ts rename to integration/tests/custom-jwt.test.ts