From 4d6b56a287523172a40e5aa26274fc08b51cd7be Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 29 May 2025 19:54:22 +0300 Subject: [PATCH 01/10] feat(e2e): Add handshake flow test for sessionsProd1 environment --- integration/presets/envs.ts | 7 ++++ integration/presets/longRunningApps.ts | 1 + integration/tests/handshake-e2e.test.ts | 49 +++++++++++++++++++++++++ package.json | 1 + 4 files changed, 58 insertions(+) create mode 100644 integration/tests/handshake-e2e.test.ts diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index eb1bbed8da6..c57b1bf7cc7 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -42,6 +42,12 @@ const withEmailCodes = base .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-email-codes').pk) .setEnvVariable('private', 'CLERK_ENCRYPTION_KEY', constants.E2E_CLERK_ENCRYPTION_KEY || 'a-key'); +const sessionsProd1 = base + .clone() + .setId('sessionsProd1') + .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('sessions-prod-1').sk) + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('sessions-prod-1').pk); + const withEmailCodes_destroy_client = withEmailCodes .clone() .setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'false'); @@ -187,4 +193,5 @@ export const envs = { withBillingStaging, withBilling, withWhatsappPhoneCode, + sessionsProd1, } as const; diff --git a/integration/presets/longRunningApps.ts b/integration/presets/longRunningApps.ts index 49ec2d7d480..392d8fbf82d 100644 --- a/integration/presets/longRunningApps.ts +++ b/integration/presets/longRunningApps.ts @@ -24,6 +24,7 @@ export const createLongRunningApps = () => { { id: 'react.vite.withEmailCodes_persist_client', config: react.vite, env: envs.withEmailCodes_destroy_client }, { id: 'react.vite.withEmailLinks', config: react.vite, env: envs.withEmailLinks }, { id: 'next.appRouter.withEmailCodes', config: next.appRouter, env: envs.withEmailCodes }, + { id: 'next.appRouter.sessionsProd1', config: next.appRouter, env: envs.sessionsProd1 }, { id: 'next.appRouter.withEmailCodes_persist_client', config: next.appRouter, diff --git a/integration/tests/handshake-e2e.test.ts b/integration/tests/handshake-e2e.test.ts new file mode 100644 index 00000000000..542b7bb3195 --- /dev/null +++ b/integration/tests/handshake-e2e.test.ts @@ -0,0 +1,49 @@ +import type { Server, ServerOptions } from 'node:https'; + +import { test } from '@playwright/test'; + +import { constants } from '../constants'; +import { fs } from '../scripts'; +import { createProxyServer } from '../scripts/proxyServer'; +import type { FakeUser } from '../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../testUtils'; + +testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('handshake flow @handshake', ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + test.describe('todo', () => { + const host = 'multiple-apps-e2e.clerk.app'; + const fakeUsers: FakeUser[] = []; + + let server: Server; + + test.afterAll(async () => { + await Promise.all(fakeUsers.map(u => u.deleteIfExists())); + server.close(); + }); + + test('apps can be used without clearing the cookies after instance switch', async ({ context }) => { + // Prepare the proxy server tha maps from the prod domain to the local apps + // We don't need to restart this one as the serverUrl will be the same for both apps + const ssl: Pick = { + cert: fs.readFileSync(constants.CERTS_DIR + '/sessions.pem'), + key: fs.readFileSync(constants.CERTS_DIR + '/sessions-key.pem'), + }; + server = createProxyServer({ ssl, targets: { [host]: app.serverUrl } }); + + const page = await context.newPage(); + const u = createTestUtils({ app, page, context }); + + const fakeUser = u.services.users.createFakeUser(); + fakeUsers.push(fakeUser); + await u.services.users.createBapiUser(fakeUser); + + await u.po.signIn.goTo(); + await u.po.signIn.setIdentifier(fakeUser.email); + await u.po.signIn.continue(); + await u.po.signIn.setPassword(fakeUser.password); + await u.po.signIn.continue(); + await u.po.expect.toBeSignedIn(); + }); + }); +}); diff --git a/package.json b/package.json index 3a7ed8e086f..beb87180a2d 100644 --- a/package.json +++ b/package.json @@ -39,6 +39,7 @@ "test:integration:expo-web": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", "test:integration:express": "E2E_APP_ID=express.* pnpm test:integration:base --grep @express", "test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", + "test:integration:handshake": "E2E_APP_ID=next.appRouter.sessionsProd1 pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", "test:integration:nuxt": "E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", From 4e7919d2441b42dbc4ee731232d72c5e67988cb4 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 29 May 2025 20:02:55 +0300 Subject: [PATCH 02/10] feat(tests): Enable web security for handshake integration tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index beb87180a2d..b6c69acb910 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "test:integration:expo-web": "E2E_APP_ID=expo.expo-web pnpm test:integration:base --grep @expo-web", "test:integration:express": "E2E_APP_ID=express.* pnpm test:integration:base --grep @express", "test:integration:generic": "E2E_APP_ID=react.vite.*,next.appRouter.withEmailCodes* pnpm test:integration:base --grep @generic", - "test:integration:handshake": "E2E_APP_ID=next.appRouter.sessionsProd1 pnpm test:integration:base --grep @handshake", + "test:integration:handshake": "DISABLE_WEB_SECURITY=true E2E_APP_ID=next.appRouter.sessionsProd1 pnpm test:integration:base --grep @handshake", "test:integration:localhost": "pnpm test:integration:base --grep @localhost", "test:integration:nextjs": "E2E_APP_ID=next.appRouter.* pnpm test:integration:base --grep @nextjs", "test:integration:nuxt": "E2E_APP_ID=nuxt.node npm run test:integration:base -- --grep @nuxt", From 867c2d437b1a82c69f39d5331db3a3ade2659efb Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 29 May 2025 21:02:07 +0300 Subject: [PATCH 03/10] feat(tests): Fix handshake.test.ts --- integration/presets/envs.ts | 3 +- integration/tests/handshake-e2e.test.ts | 49 --------------- integration/tests/handshake/handshake.test.ts | 61 +++++++++++++++++++ 3 files changed, 63 insertions(+), 50 deletions(-) delete mode 100644 integration/tests/handshake-e2e.test.ts create mode 100644 integration/tests/handshake/handshake.test.ts diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index c57b1bf7cc7..b3a87023673 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -46,7 +46,8 @@ const sessionsProd1 = base .clone() .setId('sessionsProd1') .setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('sessions-prod-1').sk) - .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('sessions-prod-1').pk); + .setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('sessions-prod-1').pk) + .setEnvVariable('public', 'CLERK_JS_URL', ''); const withEmailCodes_destroy_client = withEmailCodes .clone() diff --git a/integration/tests/handshake-e2e.test.ts b/integration/tests/handshake-e2e.test.ts deleted file mode 100644 index 542b7bb3195..00000000000 --- a/integration/tests/handshake-e2e.test.ts +++ /dev/null @@ -1,49 +0,0 @@ -import type { Server, ServerOptions } from 'node:https'; - -import { test } from '@playwright/test'; - -import { constants } from '../constants'; -import { fs } from '../scripts'; -import { createProxyServer } from '../scripts/proxyServer'; -import type { FakeUser } from '../testUtils'; -import { createTestUtils, testAgainstRunningApps } from '../testUtils'; - -testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('handshake flow @handshake', ({ app }) => { - test.describe.configure({ mode: 'serial' }); - - test.describe('todo', () => { - const host = 'multiple-apps-e2e.clerk.app'; - const fakeUsers: FakeUser[] = []; - - let server: Server; - - test.afterAll(async () => { - await Promise.all(fakeUsers.map(u => u.deleteIfExists())); - server.close(); - }); - - test('apps can be used without clearing the cookies after instance switch', async ({ context }) => { - // Prepare the proxy server tha maps from the prod domain to the local apps - // We don't need to restart this one as the serverUrl will be the same for both apps - const ssl: Pick = { - cert: fs.readFileSync(constants.CERTS_DIR + '/sessions.pem'), - key: fs.readFileSync(constants.CERTS_DIR + '/sessions-key.pem'), - }; - server = createProxyServer({ ssl, targets: { [host]: app.serverUrl } }); - - const page = await context.newPage(); - const u = createTestUtils({ app, page, context }); - - const fakeUser = u.services.users.createFakeUser(); - fakeUsers.push(fakeUser); - await u.services.users.createBapiUser(fakeUser); - - await u.po.signIn.goTo(); - await u.po.signIn.setIdentifier(fakeUser.email); - await u.po.signIn.continue(); - await u.po.signIn.setPassword(fakeUser.password); - await u.po.signIn.continue(); - await u.po.expect.toBeSignedIn(); - }); - }); -}); diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts new file mode 100644 index 00000000000..08c32f0fb25 --- /dev/null +++ b/integration/tests/handshake/handshake.test.ts @@ -0,0 +1,61 @@ +import type { Server, ServerOptions } from 'node:https'; + +import { test } from '@playwright/test'; + +import { constants } from '../../constants'; +import { fs } from '../../scripts'; +import { createProxyServer } from '../../scripts/proxyServer'; +import type { FakeUser } from '../../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; + +testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('handshake flow @handshake', ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + test.describe('todo', () => { + const host = 'multiple-apps-e2e.clerk.app:8443'; + + let fakeUser: FakeUser; + let server: Server; + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + server.close(); + }); + + test.beforeAll(async () => { + const ssl: Pick = { + cert: fs.readFileSync(constants.CERTS_DIR + '/sessions.pem'), + key: fs.readFileSync(constants.CERTS_DIR + '/sessions-key.pem'), + }; + + server = createProxyServer({ + ssl, + targets: { + [host]: app.serverUrl, + }, + }); + + const u = createTestUtils({ app, useTestingToken: false }); + fakeUser = u.services.users.createFakeUser(); + await u.services.users.createBapiUser(fakeUser); + }); + + test('apps can be used without clearing the cookies after instance switch', async ({ context }) => { + const page = await context.newPage(); + const u = createTestUtils({ app, page, context, useTestingToken: false }); + + await u.page.pause(); + + await u.page.goto(`https://${host}`); + + await u.po.signIn.goTo(); + // TODO: need to fix the type here + await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); + await u.po.expect.toBeSignedIn(); + await u.page.pause(); + + // go to the protected page + // delete the client uat cookie etc.. + }); + }); +}); From b312d22cd2bdc8b4989e23bfef5e9d0b5564f2d7 Mon Sep 17 00:00:00 2001 From: Nikos Douvlis Date: Thu, 29 May 2025 21:04:59 +0300 Subject: [PATCH 04/10] feat(tests): Remove obsolete e2e test --- .../tests/sessions/prod-app-migration.test.ts | 102 ------------------ 1 file changed, 102 deletions(-) delete mode 100644 integration/tests/sessions/prod-app-migration.test.ts diff --git a/integration/tests/sessions/prod-app-migration.test.ts b/integration/tests/sessions/prod-app-migration.test.ts deleted file mode 100644 index 86660c8bb5b..00000000000 --- a/integration/tests/sessions/prod-app-migration.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import type { Server, ServerOptions } from 'node:https'; - -import { expect, test } from '@playwright/test'; - -import { constants } from '../../constants'; -import { appConfigs } from '../../presets'; -import { fs, getPort } from '../../scripts'; -import { createProxyServer } from '../../scripts/proxyServer'; -import type { FakeUser } from '../../testUtils'; -import { createTestUtils } from '../../testUtils'; -import { getEnvForMultiAppInstance } from './utils'; - -test.describe('root and subdomain production apps @manual-run', () => { - test.describe.configure({ mode: 'serial' }); - - test.describe('multiple apps same domain for production instances', () => { - const host = 'multiple-apps-e2e.clerk.app'; - const fakeUsers: FakeUser[] = []; - - let server: Server; - - test.afterAll(async () => { - await Promise.all(fakeUsers.map(u => u.deleteIfExists())); - server.close(); - }); - - test('apps can be used without clearing the cookies after instance switch', async ({ context }) => { - // We need both apps to run on the same port - const port = await getPort(); - - const apps = await Promise.all([ - // Last version before multi-app-same-domain support - await appConfigs.next.appRouter.clone().addDependency('@clerk/nextjs', '5.2.4').commit(), - // Locally-built SDKs - await appConfigs.next.appRouter.clone().commit(), - ]); - - // Write both apps to the disk and install dependencies - await Promise.all(apps.map(a => a.setup())); - - // Start the app with the older SDK version and let it hotload clerkjs from the CF worker - let app = apps[0]; - await app.withEnv(getEnvForMultiAppInstance('sessions-prod-1').setEnvVariable('public', 'CLERK_JS_URL', '')); - await app.dev({ port }); - - // Prepare the proxy server tha maps from the prod domain to the local apps - // We don't need to restart this one as the serverUrl will be the same for both apps - const ssl: Pick = { - cert: fs.readFileSync(constants.CERTS_DIR + '/sessions.pem'), - key: fs.readFileSync(constants.CERTS_DIR + '/sessions-key.pem'), - }; - server = createProxyServer({ ssl, targets: { [host]: apps[0].serverUrl } }); - - const page = await context.newPage(); - let u = createTestUtils({ app, page, context }); - - const fakeUser = u.services.users.createFakeUser(); - fakeUsers.push(fakeUser); - await u.services.users.createBapiUser(fakeUser); - - await u.po.testingToken.setup(); - await u.page.goto(`https://${host}`); - await u.po.signIn.goTo({ timeout: 30000 }); - await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); - await u.po.expect.toBeSignedIn(); - - expect((await u.po.clerk.getClientSideUser()).primaryEmailAddress.emailAddress).toBe(fakeUser.email); - expect((await u.page.evaluate(() => fetch('/api/me').then(r => r.json()))).userId).toBe( - (await u.po.clerk.getClientSideUser()).id, - ); - - await u.page.pause(); - // TODO - // Add cookie checks - // ... - - await app.stop(); - - // Switch to and start the app with the latest SDK version - app = apps[1]; - await app.withEnv(getEnvForMultiAppInstance('sessions-prod-1')); - await app.dev({ port }); - - await page.reload(); - u = createTestUtils({ app, page, context }); - - await u.po.expect.toBeSignedIn(); - - expect((await u.po.clerk.getClientSideUser()).primaryEmailAddress.emailAddress).toBe(fakeUser.email); - expect((await u.page.evaluate(() => fetch('/api/me').then(r => r.json()))).userId).toBe( - (await u.po.clerk.getClientSideUser()).id, - ); - - await u.page.pause(); - // TODO - // Add cookie checks - // ... - - await Promise.all(apps.map(a => a.teardown())); - }); - }); -}); From 04902fb4b1977eb093ed760f49134b8aebe347b5 Mon Sep 17 00:00:00 2001 From: Jacob Foshee Date: Thu, 29 May 2025 17:57:32 -0500 Subject: [PATCH 05/10] test(handshake): test of a handshake for reason of no client uat --- integration/tests/handshake/handshake.test.ts | 22 ++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts index 08c32f0fb25..ac4f744c942 100644 --- a/integration/tests/handshake/handshake.test.ts +++ b/integration/tests/handshake/handshake.test.ts @@ -1,6 +1,6 @@ import type { Server, ServerOptions } from 'node:https'; -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { constants } from '../../constants'; import { fs } from '../../scripts'; @@ -50,12 +50,28 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands await u.po.signIn.goTo(); // TODO: need to fix the type here - await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); + await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); await u.po.expect.toBeSignedIn(); await u.page.pause(); - // go to the protected page // delete the client uat cookie etc.. + const cookies = await u.page.context().cookies(); + // console.log('cookies', cookies); + const clientUatCookies = cookies.filter(c => c.name.startsWith('__client_uat')); + expect(clientUatCookies.length).toBeGreaterThan(0); + await context.clearCookies({ name: /__client_uat.*/ }); + await u.page.pause(); + + // debug: verify that the cookies are deleted + const cookies2 = await u.page.context().cookies(); + // console.log('cookies2', cookies2); + const clientUatCookies2 = cookies2.filter(c => c.name.startsWith('__client_uat')); + expect(clientUatCookies2.length).toBe(0); + + // go to the protected page + await u.page.goToRelative('/protected'); + await u.po.expect.toBeSignedIn(); + await u.page.pause(); }); }); }); From 69ceed7a0b4adc9cb68f86b2429c4b7c37dc324e Mon Sep 17 00:00:00 2001 From: Jacob Foshee Date: Mon, 2 Jun 2025 10:49:47 -0500 Subject: [PATCH 06/10] refactor: clean up handshake test to remove pauses, debug code --- integration/tests/handshake/handshake.test.ts | 31 +++++++------------ 1 file changed, 11 insertions(+), 20 deletions(-) diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts index ac4f744c942..b4cd6298a05 100644 --- a/integration/tests/handshake/handshake.test.ts +++ b/integration/tests/handshake/handshake.test.ts @@ -1,6 +1,6 @@ import type { Server, ServerOptions } from 'node:https'; -import { expect, test } from '@playwright/test'; +import { test } from '@playwright/test'; import { constants } from '../../constants'; import { fs } from '../../scripts'; @@ -11,7 +11,8 @@ import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('handshake flow @handshake', ({ app }) => { test.describe.configure({ mode: 'serial' }); - test.describe('todo', () => { + test.describe('with Production instance', () => { + // TODO: change host name const host = 'multiple-apps-e2e.clerk.app:8443'; let fakeUser: FakeUser; @@ -23,6 +24,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands }); test.beforeAll(async () => { + // TODO: Factor out proxy server creation to helper const ssl: Pick = { cert: fs.readFileSync(constants.CERTS_DIR + '/sessions.pem'), key: fs.readFileSync(constants.CERTS_DIR + '/sessions-key.pem'), @@ -40,38 +42,27 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands await u.services.users.createBapiUser(fakeUser); }); - test('apps can be used without clearing the cookies after instance switch', async ({ context }) => { + test('when the client uat cookies are deleted', async ({ context }) => { const page = await context.newPage(); const u = createTestUtils({ app, page, context, useTestingToken: false }); - await u.page.pause(); - await u.page.goto(`https://${host}`); await u.po.signIn.goTo(); // TODO: need to fix the type here await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); await u.po.expect.toBeSignedIn(); - await u.page.pause(); - // delete the client uat cookie etc.. - const cookies = await u.page.context().cookies(); - // console.log('cookies', cookies); - const clientUatCookies = cookies.filter(c => c.name.startsWith('__client_uat')); - expect(clientUatCookies.length).toBeGreaterThan(0); + // delete the client uat cookies to force a handshake flow await context.clearCookies({ name: /__client_uat.*/ }); - await u.page.pause(); - - // debug: verify that the cookies are deleted - const cookies2 = await u.page.context().cookies(); - // console.log('cookies2', cookies2); - const clientUatCookies2 = cookies2.filter(c => c.name.startsWith('__client_uat')); - expect(clientUatCookies2.length).toBe(0); - // go to the protected page + // go to the protected page (the handshake should happen here) await u.page.goToRelative('/protected'); + await u.po.expect.toBeSignedIn(); - await u.page.pause(); + // TODO: expect to be on the protected page + // TODO: expect to have valid cookies (session, client_uat, etc) + // TODO: expect not to have temporary cookies (e.g. handshake nonce) }); }); }); From e2edff48d87723f3c8805ab1af112892c34e0995 Mon Sep 17 00:00:00 2001 From: Jacob Foshee Date: Mon, 2 Jun 2025 11:16:04 -0500 Subject: [PATCH 07/10] test(handshake): add cookie assertions and gherkin style comments --- integration/tests/handshake/handshake.test.ts | 28 ++++++++++++++----- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts index b4cd6298a05..47ff2827ba6 100644 --- a/integration/tests/handshake/handshake.test.ts +++ b/integration/tests/handshake/handshake.test.ts @@ -1,6 +1,6 @@ import type { Server, ServerOptions } from 'node:https'; -import { test } from '@playwright/test'; +import { expect, test } from '@playwright/test'; import { constants } from '../../constants'; import { fs } from '../../scripts'; @@ -24,6 +24,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands }); test.beforeAll(async () => { + // GIVEN a Production App and Clerk instance // TODO: Factor out proxy server creation to helper const ssl: Pick = { cert: fs.readFileSync(constants.CERTS_DIR + '/sessions.pem'), @@ -38,6 +39,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands }); const u = createTestUtils({ app, useTestingToken: false }); + // AND an existing user in the instance fakeUser = u.services.users.createFakeUser(); await u.services.users.createBapiUser(fakeUser); }); @@ -46,23 +48,35 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands const page = await context.newPage(); const u = createTestUtils({ app, page, context, useTestingToken: false }); + // GIVEN the user is signed into the app on the app homepage await u.page.goto(`https://${host}`); - await u.po.signIn.goTo(); // TODO: need to fix the type here await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); await u.po.expect.toBeSignedIn(); - // delete the client uat cookies to force a handshake flow + // AND the user has no client uat cookies + // (which forces a handshake flow) await context.clearCookies({ name: /__client_uat.*/ }); - // go to the protected page (the handshake should happen here) + // WHEN the user goes to the protected page + // (the handshake should happen here) await u.page.goToRelative('/protected'); + // THEN the user is signed in await u.po.expect.toBeSignedIn(); - // TODO: expect to be on the protected page - // TODO: expect to have valid cookies (session, client_uat, etc) - // TODO: expect not to have temporary cookies (e.g. handshake nonce) + // AND the user is on the protected page + expect(u.page.url()).toBe(`https://${host}/protected`); + // AND the user has valid cookies (session, client_uat, etc) + const cookies = await u.page.context().cookies(); + const clientUatCookies = cookies.filter(c => c.name.startsWith('__client_uat')); + // TODO: should we be more specific about the number of cookies? + expect(clientUatCookies.length).toBeGreaterThan(0); + const sessionCookies = cookies.filter(c => c.name.startsWith('__session')); + expect(sessionCookies.length).toBeGreaterThan(0); + // AND the user does not have temporary cookies (e.g. __clerk_handshake, __clerk_handshake_nonce) + const handshakeCookies = cookies.filter(c => c.name.includes('handshake')); + expect(handshakeCookies.length).toBe(0); }); }); }); From 2f455f4f488c42107798cc186b087335f1f261c5 Mon Sep 17 00:00:00 2001 From: Jacob Foshee Date: Mon, 2 Jun 2025 11:54:20 -0500 Subject: [PATCH 08/10] refactor: add type `FakeUserWithEmail` to try to improve the type compatibility for signing in --- integration/testUtils/index.ts | 4 ++-- integration/testUtils/usersService.ts | 2 ++ integration/tests/handshake/handshake.test.ts | 9 ++++----- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/integration/testUtils/index.ts b/integration/testUtils/index.ts index 09dde2d7660..f3ffc9fc8f6 100644 --- a/integration/testUtils/index.ts +++ b/integration/testUtils/index.ts @@ -6,10 +6,10 @@ import type { Application } from '../models/application'; import { createEmailService } from './emailService'; import { createInvitationService } from './invitationsService'; import { createOrganizationsService } from './organizationsService'; -import type { FakeOrganization, FakeUser } from './usersService'; +import type { FakeOrganization, FakeUser, FakeUserWithEmail } from './usersService'; import { createUserService } from './usersService'; -export type { FakeUser, FakeOrganization }; +export type { FakeUser, FakeUserWithEmail, FakeOrganization }; const createClerkClient = (app: Application) => { return backendCreateClerkClient({ apiUrl: app.env.privateVariables.get('CLERK_API_URL'), diff --git a/integration/testUtils/usersService.ts b/integration/testUtils/usersService.ts index 2914a15f816..8fb16178a2e 100644 --- a/integration/testUtils/usersService.ts +++ b/integration/testUtils/usersService.ts @@ -51,6 +51,8 @@ export type FakeUser = { deleteIfExists: () => Promise; }; +export type FakeUserWithEmail = FakeUser & { email: string }; + export type FakeOrganization = { name: string; organization: { id: string }; diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts index 47ff2827ba6..9b8d2d15861 100644 --- a/integration/tests/handshake/handshake.test.ts +++ b/integration/tests/handshake/handshake.test.ts @@ -5,7 +5,7 @@ import { expect, test } from '@playwright/test'; import { constants } from '../../constants'; import { fs } from '../../scripts'; import { createProxyServer } from '../../scripts/proxyServer'; -import type { FakeUser } from '../../testUtils'; +import type { FakeUserWithEmail } from '../../testUtils'; import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('handshake flow @handshake', ({ app }) => { @@ -15,7 +15,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands // TODO: change host name const host = 'multiple-apps-e2e.clerk.app:8443'; - let fakeUser: FakeUser; + let fakeUser: FakeUserWithEmail; let server: Server; test.afterAll(async () => { @@ -40,7 +40,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands const u = createTestUtils({ app, useTestingToken: false }); // AND an existing user in the instance - fakeUser = u.services.users.createFakeUser(); + fakeUser = u.services.users.createFakeUser({ withEmail: true }) as FakeUserWithEmail; await u.services.users.createBapiUser(fakeUser); }); @@ -51,8 +51,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands // GIVEN the user is signed into the app on the app homepage await u.page.goto(`https://${host}`); await u.po.signIn.goTo(); - // TODO: need to fix the type here - await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); + await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); await u.po.expect.toBeSignedIn(); // AND the user has no client uat cookies From 9d688541c1564698296331ae7a9b808662fb98af Mon Sep 17 00:00:00 2001 From: Jacob Foshee Date: Wed, 18 Jun 2025 10:47:09 -0500 Subject: [PATCH 09/10] test: verify refresh cookie present --- integration/tests/handshake/handshake.test.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts index 9b8d2d15861..191ea0e43b6 100644 --- a/integration/tests/handshake/handshake.test.ts +++ b/integration/tests/handshake/handshake.test.ts @@ -66,13 +66,15 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands await u.po.expect.toBeSignedIn(); // AND the user is on the protected page expect(u.page.url()).toBe(`https://${host}/protected`); - // AND the user has valid cookies (session, client_uat, etc) + // AND the user has valid cookies (session, client_uat, refresh, etc) const cookies = await u.page.context().cookies(); const clientUatCookies = cookies.filter(c => c.name.startsWith('__client_uat')); - // TODO: should we be more specific about the number of cookies? + // TODO: should we be more specific about the number of cookies? (some are suffixed, some are not) expect(clientUatCookies.length).toBeGreaterThan(0); const sessionCookies = cookies.filter(c => c.name.startsWith('__session')); expect(sessionCookies.length).toBeGreaterThan(0); + const refreshCookies = cookies.filter(c => c.name.startsWith('__refresh')); + expect(refreshCookies.length).toBeGreaterThan(0); // AND the user does not have temporary cookies (e.g. __clerk_handshake, __clerk_handshake_nonce) const handshakeCookies = cookies.filter(c => c.name.includes('handshake')); expect(handshakeCookies.length).toBe(0); From 43e1c165f90b689cb181e9eface9c68bdeac0a60 Mon Sep 17 00:00:00 2001 From: Jacob Foshee Date: Wed, 18 Jun 2025 11:53:07 -0500 Subject: [PATCH 10/10] docs: add info on changing host name --- integration/README.md | 14 ++++++++++++++ integration/tests/handshake/handshake.test.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/integration/README.md b/integration/README.md index 33a054f90f6..62f17c6d2e5 100644 --- a/integration/README.md +++ b/integration/README.md @@ -590,3 +590,17 @@ Before writing tests, it's important to understand how Playwright handles test i > - `VERCEL_PROJECT_ID`: Only required if you plan on running deployment tests locally. This is the Vercel project ID, and it points to an application created via the Vercel dashboard. The easiest way to get access to it is by linking a local app to the Vercel project using the Vercel CLI, and then copying the values from the `.vercel` directory. > - `VERCEL_ORG_ID`: The organization that owns the Vercel project. See above for more details. > - `VERCEL_TOKEN`: A personal access token. This corresponds to a real user running the deployment command. Attention: Be extra careful with this token as it can't be scoped to a single Vercel project, meaning that the token has access to every project in the account it belongs to. + +## Appendix + +### Production Hosts + +Production instances necessitate the use of DNS hostnames. +For example, `multiple-apps-e2e.clerk.app` facilitates subdomain testing. +During a test, a local proxy is established to direct requests from the DNS host to a local server. + +To incorporate a new hostname: + +- Provision a new `.clerk.app` host domain. +- Establish and configure a new Clerk production application. +- Update the local test certificates to encompass the new domain alongside existing ones. diff --git a/integration/tests/handshake/handshake.test.ts b/integration/tests/handshake/handshake.test.ts index 191ea0e43b6..87717403f30 100644 --- a/integration/tests/handshake/handshake.test.ts +++ b/integration/tests/handshake/handshake.test.ts @@ -12,7 +12,7 @@ testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('hands test.describe.configure({ mode: 'serial' }); test.describe('with Production instance', () => { - // TODO: change host name + // TODO: change host name (see integration/README.md#production-hosts) const host = 'multiple-apps-e2e.clerk.app:8443'; let fakeUser: FakeUserWithEmail;