Skip to content

Commit b2b144a

Browse files
committed
test(hono): add e2e proxy tests and CI integration
Add hono to the CI integration test matrix. Add e2e tests that verify proxy-enabled auth flows work correctly, including a test that proves handshake redirects use forwarded headers instead of localhost.
1 parent d0b4c4c commit b2b144a

5 files changed

Lines changed: 108 additions & 0 deletions

File tree

.github/workflows/ci.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -305,6 +305,7 @@ jobs:
305305
"nuxt",
306306
"react-router",
307307
"custom",
308+
"hono",
308309
]
309310
test-project: ["chrome"]
310311
include:

integration/presets/envs.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,11 @@ const withWaitlistMode = withEmailCodes
136136
.setEnvVariable('private', 'CLERK_SECRET_KEY', instanceKeys.get('with-waitlist-mode').sk)
137137
.setEnvVariable('public', 'CLERK_PUBLISHABLE_KEY', instanceKeys.get('with-waitlist-mode').pk);
138138

139+
const withEmailCodesProxy = withEmailCodes
140+
.clone()
141+
.setId('withEmailCodesProxy')
142+
.setEnvVariable('private', 'CLERK_PROXY_ENABLED', 'true');
143+
139144
const withSignInOrUpFlow = withEmailCodes
140145
.clone()
141146
.setId('withSignInOrUpFlow')
@@ -222,6 +227,7 @@ export const envs = {
222227
withDynamicKeys,
223228
withEmailCodes,
224229
withEmailCodes_destroy_client,
230+
withEmailCodesProxy,
225231
withEmailCodesQuickstart,
226232
withEmailLinks,
227233
withKeyless,

integration/presets/longRunningApps.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,7 @@ export const createLongRunningApps = () => {
8686
* Hono apps
8787
*/
8888
{ id: 'hono.vite.withEmailCodes', config: hono.vite, env: envs.withEmailCodes },
89+
{ id: 'hono.vite.withEmailCodesProxy', config: hono.vite, env: envs.withEmailCodesProxy },
8990
] as const;
9091

9192
const apps = configs.map(longRunningApplication);

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,13 @@ import ViteExpress from 'vite-express';
88

99
const app = new Hono();
1010

11+
const proxyEnabled = process.env.CLERK_PROXY_ENABLED === 'true';
12+
1113
app.use(
1214
'*',
1315
clerkMiddleware({
1416
publishableKey: process.env.VITE_CLERK_PUBLISHABLE_KEY,
17+
...(proxyEnabled ? { frontendApiProxy: { enabled: true } } : {}),
1518
}),
1619
);
1720

Lines changed: 97 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
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.withEmailCodesProxy] })(
8+
'frontend API proxy tests for @hono',
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.skip('proxies Frontend API requests to /__clerk path', async ({ page, context }) => {
26+
// TODO: clerkFrontendApiProxy sends Clerk-Proxy-Url with localhost origin
27+
// because it uses requestUrl directly. FAPI rejects this with 400.
28+
// Needs the same forwarded-header awareness in proxy.ts.
29+
const u = createTestUtils({ app, page, context });
30+
const url = new URL('/api/__clerk/v1/client?_is_native=false', app.serverUrl);
31+
const res = await u.page.request.get(url.toString());
32+
33+
expect(res.status()).toBe(200);
34+
const body = await res.json();
35+
expect(body).toHaveProperty('response');
36+
});
37+
38+
test('protected routes still require auth when proxy is enabled', async ({ page, context }) => {
39+
const u = createTestUtils({ app, page, context });
40+
await u.page.goToRelative('/');
41+
await u.po.signIn.waitForMounted();
42+
43+
const url = new URL('/api/protected', app.serverUrl);
44+
const res = await u.page.request.get(url.toString());
45+
expect(res.status()).toBe(401);
46+
expect(await res.text()).toBe('Unauthorized');
47+
});
48+
49+
test('authenticated requests work with proxy enabled', async ({ page, context }) => {
50+
const u = createTestUtils({ app, page, context });
51+
await u.page.goToRelative('/');
52+
53+
await u.po.signIn.waitForMounted();
54+
await u.po.signIn.setIdentifier(fakeUser.email);
55+
await u.po.signIn.continue();
56+
await u.po.signIn.setPassword(fakeUser.password);
57+
await u.po.signIn.continue();
58+
59+
await u.po.userButton.waitForMounted();
60+
61+
const url = new URL('/api/protected', app.serverUrl);
62+
const res = await u.page.request.get(url.toString());
63+
expect(res.status()).toBe(200);
64+
expect(await res.text()).toBe('Protected API response');
65+
});
66+
67+
test('handshake redirect uses forwarded headers for proxyUrl, not localhost', async () => {
68+
// This test proves that the SDK must derive proxyUrl from x-forwarded-* headers.
69+
// When a reverse proxy sits in front of the app, the raw request URL is localhost,
70+
// but the handshake redirect must point to the public origin.
71+
//
72+
// We simulate a behind-proxy scenario by sending x-forwarded-proto and x-forwarded-host
73+
// headers, with a __client_uat cookie (non-zero) but no session cookie, which forces
74+
// a handshake. The handshake redirect Location should use the forwarded origin.
75+
const url = new URL('/api/protected', app.serverUrl);
76+
const res = await fetch(url.toString(), {
77+
headers: {
78+
'x-forwarded-proto': 'https',
79+
'x-forwarded-host': 'myapp.example.com',
80+
'sec-fetch-dest': 'document',
81+
Accept: 'text/html',
82+
Cookie: '__clerk_db_jwt=needstobeset; __client_uat=1',
83+
},
84+
redirect: 'manual',
85+
});
86+
87+
// The server should respond with a 307 handshake redirect
88+
expect(res.status).toBe(307);
89+
const location = res.headers.get('location') ?? '';
90+
// The redirect must point to the public origin (from forwarded headers),
91+
// NOT to http://localhost:PORT. If the SDK uses requestUrl.origin instead
92+
// of forwarded headers, this assertion will fail.
93+
expect(location).toContain('https://myapp.example.com');
94+
expect(location).not.toContain('localhost');
95+
});
96+
},
97+
);

0 commit comments

Comments
 (0)