Skip to content

Commit 248b068

Browse files
committed
test(e2e): Expand Express testing
1 parent 9a96d73 commit 248b068

9 files changed

Lines changed: 478 additions & 1 deletion

File tree

.changeset/hot-moose-leave.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
---
2+
---

integration/presets/longRunningApps.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@ export const createLongRunningApps = () => {
8282
{ id: 'nuxt.node', config: nuxt.node, env: envs.withCustomRoles },
8383
{ id: 'react-router.node', config: reactRouter.reactRouterNode, env: envs.withEmailCodes },
8484
{ id: 'express.vite.withEmailCodes', config: express.vite, env: envs.withEmailCodes },
85+
{ id: 'express.vite.withEmailCodesProxy', config: express.vite, env: envs.withEmailCodesProxy },
86+
{ id: 'express.vite.withCustomRoles', config: express.vite, env: envs.withCustomRoles },
8587

8688
/**
8789
* Fastify apps

integration/templates/express-vite/src/client/main.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,11 +13,14 @@ document.addEventListener('DOMContentLoaded', async function () {
1313
if (clerk.isSignedIn) {
1414
document.getElementById('app')!.innerHTML = `
1515
<div id="user-button"></div>
16+
<div id="org-switcher"></div>
1617
`;
1718

1819
const userButtonDiv = document.getElementById('user-button');
19-
2020
clerk.mountUserButton(userButtonDiv);
21+
22+
const orgSwitcherDiv = document.getElementById('org-switcher');
23+
clerk.mountOrganizationSwitcher(orgSwitcherDiv);
2124
} else {
2225
document.getElementById('app')!.innerHTML = `
2326
<div id="sign-in"></div>

integration/templates/express-vite/src/server/main.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,20 @@
11
import 'dotenv/config';
22

33
import { clerkMiddleware, getAuth } from '@clerk/express';
4+
import { verifyWebhook } from '@clerk/express/webhooks';
45
import express from 'express';
56
import ViteExpress from 'vite-express';
67

78
const app = express();
89

10+
const proxyEnabled = process.env.CLERK_PROXY_ENABLED === 'true';
11+
12+
app.use(express.json());
13+
914
app.use(
1015
clerkMiddleware({
1116
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
17+
...(proxyEnabled ? { frontendApiProxy: { enabled: (url: URL) => url.pathname.startsWith('/api') } } : {}),
1218
}),
1319
);
1420

@@ -22,5 +28,28 @@ app.get('/api/protected', (req: any, res: any, _next: any) => {
2228
res.send('Protected API response');
2329
});
2430

31+
app.get('/api/me', (req: any, res: any) => {
32+
const auth = getAuth(req);
33+
res.json({
34+
userId: auth.userId,
35+
sessionId: auth.sessionId,
36+
orgId: auth.orgId ?? null,
37+
orgRole: auth.orgRole ?? null,
38+
orgSlug: auth.orgSlug ?? null,
39+
});
40+
});
41+
42+
// Must match the secret in integration/tests/express/webhook.test.ts
43+
const TEST_WEBHOOK_SECRET = 'whsec_dGVzdF9zaWduaW5nX3NlY3JldF8zMl9jaGFyc19sb25n';
44+
45+
app.post('/api/webhooks/clerk', async (req: any, res: any) => {
46+
try {
47+
const evt = await verifyWebhook(req, { signingSecret: TEST_WEBHOOK_SECRET });
48+
res.json({ success: true, type: evt.type, data: evt.data });
49+
} catch (err) {
50+
res.status(400).json({ success: false, error: err instanceof Error ? err.message : 'Unknown error' });
51+
}
52+
});
53+
2554
const port = parseInt(process.env.PORT as string) || 3002;
2655
ViteExpress.listen(app, port, () => console.log(`Server is listening on port ${port}...`));
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import { expect, test } from '@playwright/test';
2+
3+
import { appConfigs } from '../../presets';
4+
import { testAgainstRunningApps } from '../../testUtils';
5+
6+
testAgainstRunningApps({ withEnv: [appConfigs.envs.withEmailCodes] })(
7+
'error handling tests for @express',
8+
({ app }) => {
9+
test.describe.configure({ mode: 'parallel' });
10+
11+
test('direct API call without browser cookies returns null userId', async () => {
12+
const url = new URL('/api/me', app.serverUrl);
13+
const res = await fetch(url.toString());
14+
15+
expect(res.status).toBe(200);
16+
const json = await res.json();
17+
expect(json.userId).toBeNull();
18+
});
19+
20+
test('request with invalid Authorization header is handled gracefully', async () => {
21+
const url = new URL('/api/me', app.serverUrl);
22+
const res = await fetch(url.toString(), {
23+
headers: {
24+
Authorization: 'Bearer invalid_token_here',
25+
},
26+
});
27+
28+
// Clerk middleware treats an invalid bearer token as unauthenticated (not a crash)
29+
expect(res.status).toBe(200);
30+
const json = await res.json();
31+
expect(json.userId).toBeNull();
32+
});
33+
34+
test('request with malformed cookie is handled gracefully', async () => {
35+
const url = new URL('/api/me', app.serverUrl);
36+
const res = await fetch(url.toString(), {
37+
headers: {
38+
Cookie: '__session=malformed_jwt_value; __client_uat=0',
39+
},
40+
});
41+
42+
// Clerk middleware handles malformed cookies gracefully, treating the request as unauthenticated
43+
expect(res.status).toBe(200);
44+
const json = await res.json();
45+
expect(json.userId).toBeNull();
46+
});
47+
},
48+
);
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
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+
'middleware and auth object tests for @express',
9+
({ app }) => {
10+
test.describe.configure({ mode: 'parallel' });
11+
12+
let fakeUser: FakeUser;
13+
14+
test.beforeAll(async () => {
15+
const u = createTestUtils({ app });
16+
fakeUser = u.services.users.createFakeUser();
17+
await u.services.users.createBapiUser(fakeUser);
18+
});
19+
20+
test.afterAll(async () => {
21+
await fakeUser.deleteIfExists();
22+
await app.teardown();
23+
});
24+
25+
test('auth object contains userId and sessionId when signed in', async ({ page, context }) => {
26+
const u = createTestUtils({ app, page, context });
27+
await u.page.goToRelative('/');
28+
29+
await u.po.signIn.waitForMounted();
30+
await u.po.signIn.setIdentifier(fakeUser.email);
31+
await u.po.signIn.continue();
32+
await u.po.signIn.setPassword(fakeUser.password);
33+
await u.po.signIn.continue();
34+
35+
await u.po.userButton.waitForMounted();
36+
37+
const url = new URL('/api/me', app.serverUrl);
38+
const res = await u.page.request.get(url.toString());
39+
expect(res.status()).toBe(200);
40+
41+
const json = await res.json();
42+
expect(typeof json.userId).toBe('string');
43+
expect(typeof json.sessionId).toBe('string');
44+
});
45+
46+
test('auth object contains null userId when signed out', async () => {
47+
const url = new URL('/api/me', app.serverUrl);
48+
// Raw fetch has no browser cookies, simulating an unauthenticated request.
49+
const res = await fetch(url.toString());
50+
51+
expect(res.status).toBe(200);
52+
const json = await res.json();
53+
expect(json.userId).toBeNull();
54+
expect(json.sessionId).toBeNull();
55+
});
56+
57+
test('multiple sequential requests maintain session', async ({ page, context }) => {
58+
const u = createTestUtils({ app, page, context });
59+
await u.page.goToRelative('/');
60+
61+
await u.po.signIn.waitForMounted();
62+
await u.po.signIn.setIdentifier(fakeUser.email);
63+
await u.po.signIn.continue();
64+
await u.po.signIn.setPassword(fakeUser.password);
65+
await u.po.signIn.continue();
66+
67+
await u.po.userButton.waitForMounted();
68+
69+
const url = new URL('/api/me', app.serverUrl);
70+
71+
const res1 = await u.page.request.get(url.toString());
72+
const json1 = await res1.json();
73+
74+
const res2 = await u.page.request.get(url.toString());
75+
const json2 = await res2.json();
76+
77+
expect(json1.userId).toBeTruthy();
78+
expect(json1.sessionId).toBeTruthy();
79+
expect(json1.userId).toBe(json2.userId);
80+
expect(json1.sessionId).toBe(json2.sessionId);
81+
});
82+
},
83+
);
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import type { OrganizationMembershipRole } from '@clerk/backend';
2+
import { expect, test } from '@playwright/test';
3+
4+
import { appConfigs } from '../../presets';
5+
import type { FakeOrganization, FakeUser } from '../../testUtils';
6+
import { createTestUtils, testAgainstRunningApps } from '../../testUtils';
7+
8+
testAgainstRunningApps({ withEnv: [appConfigs.envs.withCustomRoles] })(
9+
'organization auth tests for @express',
10+
({ app }) => {
11+
test.describe.configure({ mode: 'serial' });
12+
13+
let fakeAdmin: FakeUser;
14+
let fakeViewer: FakeUser;
15+
let fakeNonMember: FakeUser;
16+
let fakeOrganization: FakeOrganization;
17+
18+
test.beforeAll(async () => {
19+
const m = createTestUtils({ app });
20+
fakeAdmin = m.services.users.createFakeUser();
21+
const admin = await m.services.users.createBapiUser(fakeAdmin);
22+
fakeOrganization = await m.services.users.createFakeOrganization(admin.id);
23+
fakeViewer = m.services.users.createFakeUser();
24+
const viewer = await m.services.users.createBapiUser(fakeViewer);
25+
await m.services.clerk.organizations.createOrganizationMembership({
26+
organizationId: fakeOrganization.organization.id,
27+
role: 'org:viewer' as OrganizationMembershipRole,
28+
userId: viewer.id,
29+
});
30+
fakeNonMember = m.services.users.createFakeUser();
31+
await m.services.users.createBapiUser(fakeNonMember);
32+
});
33+
34+
test.afterAll(async () => {
35+
await fakeOrganization.delete();
36+
await fakeNonMember.deleteIfExists();
37+
await fakeViewer.deleteIfExists();
38+
await fakeAdmin.deleteIfExists();
39+
await app.teardown();
40+
});
41+
42+
test('admin auth object includes orgId, orgRole, orgSlug after selecting org', async ({ page, context }) => {
43+
const u = createTestUtils({ app, page, context });
44+
await u.page.goToRelative('/');
45+
46+
await u.po.signIn.waitForMounted();
47+
await u.po.signIn.setIdentifier(fakeAdmin.email);
48+
await u.po.signIn.continue();
49+
await u.po.signIn.setPassword(fakeAdmin.password);
50+
await u.po.signIn.continue();
51+
52+
await u.po.userButton.waitForMounted();
53+
54+
await u.po.organizationSwitcher.waitForMounted();
55+
await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
56+
57+
const url = new URL('/api/me', app.serverUrl);
58+
const res = await u.page.request.get(url.toString());
59+
expect(res.status()).toBe(200);
60+
61+
const json = await res.json();
62+
expect(json.userId).toBeTruthy();
63+
expect(json.orgId).toBe(fakeOrganization.organization.id);
64+
expect(json.orgRole).toBe('org:admin');
65+
expect(json.orgSlug).toBeTruthy();
66+
});
67+
68+
test('non-member auth object has null orgId', async ({ page, context }) => {
69+
const u = createTestUtils({ app, page, context });
70+
await u.page.goToRelative('/');
71+
72+
await u.po.signIn.waitForMounted();
73+
await u.po.signIn.setIdentifier(fakeNonMember.email);
74+
await u.po.signIn.continue();
75+
await u.po.signIn.setPassword(fakeNonMember.password);
76+
await u.po.signIn.continue();
77+
78+
await u.po.userButton.waitForMounted();
79+
80+
const url = new URL('/api/me', app.serverUrl);
81+
const res = await u.page.request.get(url.toString());
82+
expect(res.status()).toBe(200);
83+
84+
const json = await res.json();
85+
expect(json.userId).toBeTruthy();
86+
expect(json.orgId).toBeNull();
87+
});
88+
89+
test('viewer org role is correctly reflected in auth response', async ({ page, context }) => {
90+
const u = createTestUtils({ app, page, context });
91+
await u.page.goToRelative('/');
92+
93+
await u.po.signIn.waitForMounted();
94+
await u.po.signIn.setIdentifier(fakeViewer.email);
95+
await u.po.signIn.continue();
96+
await u.po.signIn.setPassword(fakeViewer.password);
97+
await u.po.signIn.continue();
98+
99+
await u.po.userButton.waitForMounted();
100+
101+
await u.po.organizationSwitcher.waitForMounted();
102+
await u.po.organizationSwitcher.waitForAnOrganizationToSelected();
103+
104+
const url = new URL('/api/me', app.serverUrl);
105+
const res = await u.page.request.get(url.toString());
106+
expect(res.status()).toBe(200);
107+
108+
const json = await res.json();
109+
expect(json.userId).toBeTruthy();
110+
expect(json.orgId).toBe(fakeOrganization.organization.id);
111+
expect(json.orgRole).toBe('org:viewer');
112+
});
113+
},
114+
);

0 commit comments

Comments
 (0)