Skip to content

Commit aac5250

Browse files
committed
test(integration): add multi-session token refresh tests
Add integration tests proving that in a multi-session scenario, each session always gets its own correct token — not a token belonging to whichever session was last active. Test 1 (fast): Verifies FAPI token fetch returns a JWT with the correct sid claim for each session after switching between them. Test 2 (slow, ~70s): Verifies server-side middleware refresh/handshake preserves the correct active session after the __session cookie JWT expires, rather than swapping to the most recently touched session.
1 parent 6659e3a commit aac5250

1 file changed

Lines changed: 193 additions & 0 deletions

File tree

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { appConfigs } from '../presets';
4+
import type { FakeUser } from '../testUtils';
5+
import { createTestUtils, testAgainstRunningApps } from '../testUtils';
6+
7+
testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
8+
'multi-session token refresh @nextjs',
9+
({ app }) => {
10+
test.describe.configure({ mode: 'serial' });
11+
12+
let fakeUser1: FakeUser;
13+
let fakeUser2: FakeUser;
14+
15+
test.beforeAll(async () => {
16+
const u = createTestUtils({ app });
17+
fakeUser1 = u.services.users.createFakeUser();
18+
fakeUser2 = u.services.users.createFakeUser();
19+
await u.services.users.createBapiUser(fakeUser1);
20+
await u.services.users.createBapiUser(fakeUser2);
21+
});
22+
23+
test.afterAll(async () => {
24+
await fakeUser1.deleteIfExists();
25+
await fakeUser2.deleteIfExists();
26+
await app.teardown();
27+
});
28+
29+
test('FAPI token fetch returns correct sid per session', async ({ page, context }) => {
30+
const u = createTestUtils({ app, page, context });
31+
32+
// Sign in user1 via the UI
33+
await u.po.signIn.goTo();
34+
await u.po.signIn.signInWithEmailAndInstantPassword({
35+
email: fakeUser1.email,
36+
password: fakeUser1.password,
37+
});
38+
await u.po.expect.toBeSignedIn();
39+
40+
const session1Info = await page.evaluate(() => {
41+
const clerk = (window as any).Clerk;
42+
return {
43+
sessionId: clerk.session.id,
44+
userId: clerk.user.id,
45+
};
46+
});
47+
48+
expect(session1Info.sessionId).toBeDefined();
49+
50+
// Sign in user2 programmatically to create a second session
51+
const signInResult = await page.evaluate(
52+
async ({ email, password }) => {
53+
const clerk = (window as any).Clerk;
54+
const signIn = await clerk.client.signIn.create({ identifier: email, password });
55+
await clerk.setActive({ session: signIn.createdSessionId });
56+
return {
57+
sessionCount: clerk.client.sessions.length,
58+
success: true,
59+
};
60+
},
61+
{ email: fakeUser2.email, password: fakeUser2.password },
62+
);
63+
64+
expect(signInResult.success).toBe(true);
65+
expect(signInResult.sessionCount).toBe(2);
66+
67+
const session2Info = await page.evaluate(() => {
68+
const clerk = (window as any).Clerk;
69+
return {
70+
sessionId: clerk.session.id,
71+
userId: clerk.user.id,
72+
};
73+
});
74+
75+
expect(session2Info.sessionId).toBeDefined();
76+
expect(session2Info.sessionId).not.toBe(session1Info.sessionId);
77+
78+
// Switch to session1, fetch token, decode JWT, assert sid matches session1
79+
const token1Sid = await page.evaluate(async ({ sessionId }) => {
80+
const clerk = (window as any).Clerk;
81+
await clerk.setActive({ session: sessionId });
82+
const token = await clerk.session.getToken({ skipCache: true });
83+
const payload = JSON.parse(atob(token.split('.')[1]));
84+
return payload.sid;
85+
}, session1Info);
86+
87+
expect(token1Sid).toBe(session1Info.sessionId);
88+
89+
// Switch to session2, fetch token, decode JWT, assert sid matches session2
90+
const token2Sid = await page.evaluate(async ({ sessionId }) => {
91+
const clerk = (window as any).Clerk;
92+
await clerk.setActive({ session: sessionId });
93+
const token = await clerk.session.getToken({ skipCache: true });
94+
const payload = JSON.parse(atob(token.split('.')[1]));
95+
return payload.sid;
96+
}, session2Info);
97+
98+
expect(token2Sid).toBe(session2Info.sessionId);
99+
});
100+
101+
test('server-side refresh preserves correct session after token expiry', async ({ page, context }) => {
102+
// This test waits ~65s for JWT expiry, so triple the default timeout
103+
test.slow();
104+
105+
const u = createTestUtils({ app, page, context });
106+
107+
// Sign in user1 via the UI
108+
await u.po.signIn.goTo();
109+
await u.po.signIn.signInWithEmailAndInstantPassword({
110+
email: fakeUser1.email,
111+
password: fakeUser1.password,
112+
});
113+
await u.po.expect.toBeSignedIn();
114+
115+
const session1Info = await page.evaluate(() => {
116+
const clerk = (window as any).Clerk;
117+
return {
118+
sessionId: clerk.session.id,
119+
userId: clerk.user.id,
120+
};
121+
});
122+
123+
expect(session1Info.sessionId).toBeDefined();
124+
125+
// Sign in user2 programmatically → session2 becomes "last active"
126+
await page.evaluate(
127+
async ({ email, password }) => {
128+
const clerk = (window as any).Clerk;
129+
const signIn = await clerk.client.signIn.create({ identifier: email, password });
130+
await clerk.setActive({ session: signIn.createdSessionId });
131+
},
132+
{ email: fakeUser2.email, password: fakeUser2.password },
133+
);
134+
135+
const session2Info = await page.evaluate(() => {
136+
const clerk = (window as any).Clerk;
137+
return {
138+
sessionId: clerk.session.id,
139+
userId: clerk.user.id,
140+
};
141+
});
142+
143+
expect(session2Info.sessionId).not.toBe(session1Info.sessionId);
144+
145+
// Fetch a token for session2 to ensure it's the most recently touched
146+
await page.evaluate(async () => {
147+
const clerk = (window as any).Clerk;
148+
await clerk.session.getToken({ skipCache: true });
149+
});
150+
151+
// Switch back to session1 as the active session
152+
await page.evaluate(async ({ sessionId }) => {
153+
const clerk = (window as any).Clerk;
154+
await clerk.setActive({ session: sessionId });
155+
}, session1Info);
156+
157+
// Verify we're on session1
158+
const activeBeforeWait = await page.evaluate(() => {
159+
const clerk = (window as any).Clerk;
160+
return clerk.session.id;
161+
});
162+
expect(activeBeforeWait).toBe(session1Info.sessionId);
163+
164+
// Block client-side FAPI token refreshes to prevent the SessionCookiePoller
165+
// from refreshing the __session cookie before it expires
166+
await page.route('**/v1/client/sessions/*/tokens*', route => route.abort());
167+
168+
// Wait ~65s for the JWT in the __session cookie to expire
169+
// (dev instance token lifetime is 60s)
170+
// eslint-disable-next-line playwright/no-wait-for-timeout
171+
await page.waitForTimeout(65_000);
172+
173+
// Remove route interception, then navigate to trigger server-side refresh
174+
await page.unroute('**/v1/client/sessions/*/tokens*');
175+
await page.goto(app.serverUrl);
176+
177+
// Wait for Clerk to be fully loaded after the server-side refresh/handshake
178+
await page.waitForFunction(() => (window as any).Clerk?.loaded);
179+
180+
// Assert the active session is still session1, not swapped to session2
181+
const sessionAfterRefresh = await page.evaluate(() => {
182+
const clerk = (window as any).Clerk;
183+
return {
184+
sessionId: clerk.session?.id,
185+
userId: clerk.user?.id,
186+
};
187+
});
188+
189+
expect(sessionAfterRefresh.sessionId).toBe(session1Info.sessionId);
190+
expect(sessionAfterRefresh.userId).toBe(session1Info.userId);
191+
});
192+
},
193+
);

0 commit comments

Comments
 (0)