Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions integration/presets/envs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,14 @@ const withEmailCodesQuickstart = withEmailCodes
.setEnvVariable('public', 'CLERK_SIGN_IN_URL', '')
.setEnvVariable('public', 'CLERK_SIGN_UP_URL', '');

// Uses staging instance which runs Core 3
const withAPCore3ClerkV5 = environmentConfig()
.setId('withAPCore3ClerkV5')
.setEnvVariable('public', 'CLERK_TELEMETRY_DISABLED', true)
.setEnvVariable('private', 'CLERK_API_URL', 'https://api.clerkstage.dev')
.setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-billing-staging').sk)
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-billing-staging').pk);

// Uses staging instance which runs Core 3
const withAPCore3ClerkV6 = environmentConfig()
.setId('withAPCore3ClerkV6')
Expand Down Expand Up @@ -206,6 +214,7 @@ export const envs = {
sessionsProd1,
withAPIKeys,
withAPCore3ClerkLatest,
withAPCore3ClerkV5,
withAPCore3ClerkV6,
withBilling,
withBillingJwtV2,
Expand Down
6 changes: 6 additions & 0 deletions integration/presets/next.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,11 @@ const appRouterQuickstartV6 = appRouter
.setName('next-app-router-quickstart-v6')
.useTemplate(templates['next-app-router-quickstart-v6']);

const appRouterAPWithClerkNextV5 = appRouterQuickstartV6
.clone()
.setName('next-app-router-ap-clerk-next-v5')
.addDependency('@clerk/nextjs', '5');

const appRouterAPWithClerkNextV6 = appRouterQuickstartV6
.clone()
.setName('next-app-router-ap-clerk-next-v6')
Expand Down Expand Up @@ -67,6 +72,7 @@ export const next = {
appRouterTurbo,
appRouterQuickstart,
appRouterAPWithClerkNextLatest,
appRouterAPWithClerkNextV5,
appRouterAPWithClerkNextV6,
appRouterQuickstartV6,
appRouterBundledUI,
Expand Down
41 changes: 41 additions & 0 deletions integration/tests/next-account-portal/clerk-ap-core-3-v5.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { test } from '@playwright/test';

import type { Application } from '../../models/application';
import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { testSignIn, testSignUp, testSSR } from './common';

test.describe('Next with ClerkJS V5 <-> Account Portal Core 3 @ap-flows', () => {
test.describe.configure({ mode: 'serial' });
let app: Application;
let fakeUser: FakeUser;

test.beforeAll(async () => {
test.setTimeout(90_000); // Wait for app to be ready
app = await appConfigs.next.appRouterAPWithClerkNextV5.clone().commit();
await app.setup();
await app.withEnv(appConfigs.envs.withAPCore3ClerkV5);
await app.dev();
const u = createTestUtils({ app });
fakeUser = u.services.users.createFakeUser();
await u.services.users.createBapiUser(fakeUser);
});

test.afterAll(async () => {
await fakeUser.deleteIfExists();
await app.teardown();
});

test('sign in', async ({ page, context }) => {
await testSignIn({ app, page, context, fakeUser });
});

test('sign up', async ({ page, context }) => {
await testSignUp({ app, page, context, fakeUser });
});

test('ssr', async ({ page, context }) => {
await testSSR({ app, page, context, fakeUser });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Application } from '../../models/application';
import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { testSignIn, testSignUp, testSSR } from './common';
import { testHandshakeRecovery, testSignIn, testSignOut, testSignUp, testSSR } from './common';

test.describe('Next with ClerkJS V6 <-> Account Portal Core 3 @ap-flows', () => {
test.describe.configure({ mode: 'serial' });
Expand Down Expand Up @@ -38,4 +38,12 @@ test.describe('Next with ClerkJS V6 <-> Account Portal Core 3 @ap-flows', () =>
test('ssr', async ({ page, context }) => {
await testSSR({ app, page, context, fakeUser });
});

test('sign out clears session and AP state', async ({ page, context }) => {
await testSignOut({ app, page, context, fakeUser });
});

test('handshake recovery after session cookie loss', async ({ page, context }) => {
await testHandshakeRecovery({ app, page, context, fakeUser });
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type { Application } from '../../models/application';
import { appConfigs } from '../../presets';
import type { FakeUser } from '../../testUtils';
import { createTestUtils } from '../../testUtils';
import { testSignIn, testSignUp, testSSR } from './common';
import { testHandshakeRecovery, testSignIn, testSignOut, testSignUp, testSSR } from './common';

test.describe('Next with ClerkJS V7 <-> Account Portal Core 3 @ap-flows', () => {
test.describe.configure({ mode: 'serial' });
Expand Down Expand Up @@ -38,4 +38,12 @@ test.describe('Next with ClerkJS V7 <-> Account Portal Core 3 @ap-flows', () =>
test('ssr', async ({ page, context }) => {
await testSSR({ app, page, context, fakeUser });
});

test('sign out clears session and AP state', async ({ page, context }) => {
await testSignOut({ app, page, context, fakeUser });
});

test('handshake recovery after session cookie loss', async ({ page, context }) => {
await testHandshakeRecovery({ app, page, context, fakeUser });
});
});
86 changes: 86 additions & 0 deletions integration/tests/next-account-portal/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -182,3 +182,89 @@ export const testSSR = async ({ app, page, context, fakeUser }: TestParams) => {

expect(await u.po.userButton.waitForMounted()).not.toBeUndefined();
};

export const testSignOut = async ({ app, page, context, fakeUser }: TestParams) => {
const u = createTestUtils({ app, page, context, useTestingToken: false });

// Sign in via Account Portal first
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.page.getByRole('button', { name: /Sign in/i }).click();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.page.waitForAppUrl('/');
await u.po.expect.toBeSignedIn();
await u.po.userButton.waitForMounted();

// Verify session cookie is set before sign-out
const sessionBefore = await context
.cookies(page.url())
.then(cookies => cookies.find(c => c.name === CLERK_SESSION_COOKIE_NAME)?.value);
expect(!!sessionBefore).toBeTruthy();

// Sign out via Clerk.signOut()
await page.evaluate(() => window.Clerk.signOut());
await u.po.expect.toBeSignedOut();

// Verify session cookie is cleared
const sessionAfter = await context
.cookies(page.url())
.then(cookies => cookies.find(c => c.name === CLERK_SESSION_COOKIE_NAME)?.value);
expect(!!sessionAfter).toBeFalsy();

// Reload and verify user stays signed out (no auto-sign-in from stale state)
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

// Navigate to AP again and verify sign-in form is shown (not auto-signed-in)
await u.page.getByRole('button', { name: /Sign in/i }).click();
await u.po.signIn.waitForMounted();
const apURL = page.url();
expect(apURL).toMatch(/\.accounts(stage\.dev|\.dev|\.stg)/);
};

export const testHandshakeRecovery = async ({ app, page, context, fakeUser }: TestParams) => {
const u = createTestUtils({ app, page, context, useTestingToken: false });

// Sign in via Account Portal
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();
await u.po.expect.toBeSignedOut();

await u.page.getByRole('button', { name: /Sign in/i }).click();
await u.po.signIn.waitForMounted();
await u.po.signIn.signInWithEmailAndInstantPassword({ email: fakeUser.email, password: fakeUser.password });
await u.page.waitForAppUrl('/');
await u.po.expect.toBeSignedIn();

// Delete the __session cookie to simulate an expired/invalid session.
// Keep __client_uat so the middleware detects a mismatch and triggers a handshake.
const appUrl = new URL(page.url());
await context.clearCookies({ name: CLERK_SESSION_COOKIE_NAME, domain: appUrl.hostname });

// Reload the page. The middleware should:
// 1. Detect missing session + present client_uat
// 2. Trigger a handshake redirect to FAPI
// 3. FAPI resolves the handshake and returns fresh cookies
// 4. User ends up signed in again (no redirect loop, no error)
await u.page.goToAppHome();
await u.page.waitForClerkJsLoaded();

// The page should load successfully (not stuck in a redirect loop).
// The user should be signed in because the handshake recovered the session.
await u.po.expect.toBeSignedIn();

// Verify the session cookie was re-established by the handshake
const sessionAfterRecovery = await context
.cookies(page.url())
.then(cookies => cookies.find(c => c.name === CLERK_SESSION_COOKIE_NAME)?.value);
expect(!!sessionAfterRecovery).toBeTruthy();

// Verify no leftover handshake params in the URL
const finalURL = new URL(page.url());
expect(finalURL.searchParams.has('__clerk_handshake')).toBeFalsy();
expect(finalURL.searchParams.has('__clerk_handshake_nonce')).toBeFalsy();
};
Loading