diff --git a/integration/presets/envs.ts b/integration/presets/envs.ts index eb1bbed8da6..b3a87023673 100644 --- a/integration/presets/envs.ts +++ b/integration/presets/envs.ts @@ -42,6 +42,13 @@ 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) + .setEnvVariable('public', 'CLERK_JS_URL', ''); + const withEmailCodes_destroy_client = withEmailCodes .clone() .setEnvVariable('public', 'EXPERIMENTAL_PERSIST_CLIENT', 'false'); @@ -187,4 +194,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/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 new file mode 100644 index 00000000000..9b8d2d15861 --- /dev/null +++ b/integration/tests/handshake/handshake.test.ts @@ -0,0 +1,81 @@ +import type { Server, ServerOptions } from 'node:https'; + +import { expect, test } from '@playwright/test'; + +import { constants } from '../../constants'; +import { fs } from '../../scripts'; +import { createProxyServer } from '../../scripts/proxyServer'; +import type { FakeUserWithEmail } from '../../testUtils'; +import { createTestUtils, testAgainstRunningApps } from '../../testUtils'; + +testAgainstRunningApps({ withPattern: ['next.appRouter.sessionsProd1'] })('handshake flow @handshake', ({ app }) => { + test.describe.configure({ mode: 'serial' }); + + test.describe('with Production instance', () => { + // TODO: change host name + const host = 'multiple-apps-e2e.clerk.app:8443'; + + let fakeUser: FakeUserWithEmail; + let server: Server; + + test.afterAll(async () => { + await fakeUser.deleteIfExists(); + server.close(); + }); + + 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'), + key: fs.readFileSync(constants.CERTS_DIR + '/sessions-key.pem'), + }; + + server = createProxyServer({ + ssl, + targets: { + [host]: app.serverUrl, + }, + }); + + const u = createTestUtils({ app, useTestingToken: false }); + // AND an existing user in the instance + fakeUser = u.services.users.createFakeUser({ withEmail: true }) as FakeUserWithEmail; + await u.services.users.createBapiUser(fakeUser); + }); + + test('when the client uat cookies are deleted', async ({ context }) => { + 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(); + await u.po.signIn.signInWithEmailAndInstantPassword(fakeUser); + await u.po.expect.toBeSignedIn(); + + // AND the user has no client uat cookies + // (which forces a handshake flow) + await context.clearCookies({ name: /__client_uat.*/ }); + + // 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(); + // 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); + }); + }); +}); 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())); - }); - }); -}); diff --git a/package.json b/package.json index 766bf8231ef..87d326342d0 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": "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",