Skip to content

Commit d53f9b0

Browse files
authored
test(e2e): run e2e against production build (#283)
1 parent c689511 commit d53f9b0

File tree

9 files changed

+66
-23
lines changed

9 files changed

+66
-23
lines changed

.github/workflows/e2e.yml

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,11 @@ jobs:
2222
OIDC_ISSUER_URL: http://localhost:4000
2323
OIDC_CLIENT_ID: test-only-not-a-real-id
2424
OIDC_CLIENT_SECRET: test-only-not-a-real-secret
25-
NEXT_PUBLIC_OIDC_PROVIDER_ID: oidc
25+
OIDC_PROVIDER_ID: oidc
2626
BETTER_AUTH_URL: http://localhost:3000
2727
BETTER_AUTH_SECRET: test-only-not-a-real-better-auth-secret
28-
USE_OLLAMA: "true"
29-
OLLAMA_MODEL: qwen2.5:1.5b
28+
USE_E2E_MODEL: "true"
29+
E2E_MODEL_NAME: qwen2.5:1.5b
3030
OLLAMA_BASE_URL: http://localhost:11434
3131
steps:
3232
- name: Checkout
@@ -64,6 +64,13 @@ jobs:
6464
- name: Install Playwright browsers
6565
run: pnpm exec playwright install --with-deps chromium
6666

67+
- name: Build production app
68+
run: pnpm build
69+
env:
70+
# These env vars must match the runtime values for token encryption/decryption to work
71+
BETTER_AUTH_SECRET: test-only-not-a-real-better-auth-secret
72+
OIDC_PROVIDER_ID: oidc
73+
6774
- name: Run Playwright tests
6875
run: pnpm test:e2e
6976

CLAUDE.md

Lines changed: 5 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -224,13 +224,12 @@ pnpm generate-client:nofetch # Regenerate without fetching
224224

225225
### E2E Tests (Playwright)
226226

227-
- End-to-end tests live under `tests/e2e` and run against a live dev stack.
227+
- End-to-end tests live under `tests/e2e` and run against a **production build**.
228228
- Commands:
229-
- `pnpm dev` – starts Next.js (3000), mock OIDC (4000), and MSW mock API (9090)
230-
- `pnpm run test:e2e` – runs Playwright tests (headless)
231-
- `pnpm run test:e2e:ui` – opens Playwright UI mode for interactive debugging
232-
- `pnpm run test:e2e:debug` – runs with Playwright Inspector
233-
- CI runs E2E tests via `.github/workflows/bdd.yml` and installs Playwright browsers.
229+
- `pnpm test:e2e` – builds the app and runs E2E tests
230+
- `pnpm test:e2e:ui` – builds and opens Playwright UI mode for interactive debugging
231+
- `pnpm test:e2e:debug` – builds and runs with Playwright Inspector
232+
- CI runs E2E tests via `.github/workflows/e2e.yml` (builds first, then tests)
234233
- Install browsers locally once: `pnpm exec playwright install`
235234

236235
Tests use custom fixtures for authentication. The `authenticatedPage` fixture handles login automatically.

dev-auth/oidc-provider.mjs

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,9 @@
1010
import { config } from "dotenv";
1111
import Provider from "oidc-provider";
1212

13-
config();
14-
config({ path: ".env.local" });
13+
// Load env files but don't override existing env vars (e.g., from playwright)
14+
config({ override: false });
15+
config({ path: ".env.local", override: false });
1516

1617
const ISSUER = process.env.OIDC_ISSUER_URL || "http://localhost:4000";
1718
const PORT = new URL(ISSUER).port || 4000;
@@ -106,9 +107,12 @@ const configuration = {
106107
profile: ["name"],
107108
},
108109
ttl: {
109-
// Short-lived access tokens to force refresh during dev
110-
AccessToken: 15, // seconds
110+
AccessToken: 15, // seconds - short-lived to exercise refresh flow
111111
RefreshToken: 86400 * 30, // 30 days
112+
Interaction: 3600, // 1 hour
113+
Session: 86400 * 14, // 14 days
114+
Grant: 86400 * 14, // 14 days
115+
IdToken: 3600, // 1 hour
112116
},
113117
// Dev-only: always issue refresh tokens to make the flow reliable locally
114118
issueRefreshToken: async () => true,
@@ -119,7 +123,6 @@ const oidc = new Provider(ISSUER, configuration);
119123
// Simple interaction endpoint for dev - auto-login as test-user
120124
oidc.use(async (ctx, next) => {
121125
if (ctx.path.startsWith("/interaction/")) {
122-
const _uid = ctx.path.split("/")[2];
123126
const interaction = await oidc.interactionDetails(ctx.req, ctx.res);
124127

125128
if (interaction.prompt.name === "login") {

package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,9 +13,10 @@
1313
"lint": "biome check",
1414
"format": "biome format --write",
1515
"test": "vitest",
16-
"test:e2e": "playwright test",
17-
"test:e2e:ui": "playwright test --ui",
18-
"test:e2e:debug": "playwright test --debug",
16+
"test:e2e": "BETTER_AUTH_SECRET=e2e-test-secret-at-least-32-chars-long OIDC_PROVIDER_ID=okta BETTER_AUTH_RATE_LIMIT=100 pnpm build && playwright test",
17+
"test:e2e:ui": "BETTER_AUTH_SECRET=e2e-test-secret-at-least-32-chars-long OIDC_PROVIDER_ID=okta BETTER_AUTH_RATE_LIMIT=100 pnpm build && playwright test --ui",
18+
"test:e2e:debug": "BETTER_AUTH_SECRET=e2e-test-secret-at-least-32-chars-long OIDC_PROVIDER_ID=okta BETTER_AUTH_RATE_LIMIT=100 pnpm build && playwright test --debug",
19+
"start:e2e": "concurrently -n \"OIDC,Mock,Next\" -c \"blue,magenta,green\" \"pnpm oidc\" \"pnpm mock:server\" \"pnpm start\"",
1920
"test:coverage": "vitest run --coverage",
2021
"type-check": "tsc --noEmit",
2122
"prepare": "husky",

playwright.config.mts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ export default defineConfig({
1818
fullyParallel: true,
1919
forbidOnly: !!process.env.CI,
2020
retries: process.env.CI ? 2 : 0,
21-
workers: process.env.CI ? 1 : undefined,
2221
reporter: process.env.CI ? "github" : "list",
2322
timeout: 30_000,
2423
use: {
@@ -35,7 +34,8 @@ export default defineConfig({
3534
webServer: serverAlreadyRunning
3635
? undefined
3736
: {
38-
command: "pnpm dev",
37+
// Run against production build - requires `pnpm build` to be run first
38+
command: "pnpm start:e2e",
3939
url: BASE_URL,
4040
timeout: 120_000,
4141
stdout: "pipe",
@@ -45,9 +45,13 @@ export default defineConfig({
4545
OIDC_ISSUER_URL: "http://localhost:4000",
4646
OIDC_CLIENT_ID: "better-auth-dev",
4747
OIDC_CLIENT_SECRET: "dev-secret-change-in-production",
48-
NEXT_PUBLIC_OIDC_PROVIDER_ID: "okta",
48+
OIDC_PROVIDER_ID: "okta",
4949
BETTER_AUTH_URL: "http://localhost:3000",
5050
BETTER_AUTH_SECRET: "e2e-test-secret-at-least-32-chars-long",
51+
// Better Auth rate limits sign-in to 3 requests per 10 seconds by default.
52+
// E2E tests with multiple authenticatedPage fixtures exceed this limit,
53+
// causing 429 errors. Set to 100 to allow rapid sequential logins.
54+
BETTER_AUTH_RATE_LIMIT: "100",
5155
// Always use testing model for E2E tests to avoid needing OpenRouter API keys
5256
USE_E2E_MODEL: "true",
5357
E2E_MODEL_NAME: process.env.E2E_MODEL_NAME ?? "qwen2.5:1.5b",

src/lib/auth/auth.ts

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import { type Auth, type BetterAuthOptions, betterAuth } from "better-auth";
33
import { genericOAuth } from "better-auth/plugins";
44
import {
55
BASE_URL,
6+
BETTER_AUTH_RATE_LIMIT,
67
BETTER_AUTH_SECRET,
78
IS_PRODUCTION,
89
OIDC_CLIENT_ID,
@@ -162,6 +163,21 @@ export const auth: Auth<BetterAuthOptions> = betterAuth({
162163
secret: BETTER_AUTH_SECRET,
163164
baseURL: BASE_URL,
164165
...(pool && { database: pool }),
166+
// Rate limit override for E2E tests.
167+
// Better Auth's default rate limit for /sign-in/* is 3 requests per 10 seconds.
168+
// This is too restrictive for E2E tests where multiple tests authenticate in
169+
// quick succession. We use customRules because the default special rules for
170+
// sign-in paths take precedence over the global max setting.
171+
...(BETTER_AUTH_RATE_LIMIT && {
172+
rateLimit: {
173+
customRules: {
174+
"/sign-in/*": {
175+
max: BETTER_AUTH_RATE_LIMIT,
176+
window: 10,
177+
},
178+
},
179+
},
180+
}),
165181
account: {
166182
storeStateStrategy: pool ? "database" : "cookie",
167183
storeAccountCookie: !pool,

src/lib/auth/constants.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,19 @@ export const COOKIE_SECURE =
5050
// Database configuration (optional - enables database mode for large OIDC tokens)
5151
export const DATABASE_URL = process.env.DATABASE_URL;
5252

53+
// Rate limiting configuration
54+
//
55+
// Better Auth has a default rate limit of 3 requests per 10 seconds for sign-in
56+
// endpoints. This causes E2E test failures when multiple tests authenticate in
57+
// quick succession (e.g., 3 tests using authenticatedPage fixture followed by
58+
// a login test = 4 sign-ins, triggering 429 Too Many Requests).
59+
//
60+
// Set BETTER_AUTH_RATE_LIMIT to a higher value (e.g., 100) for E2E tests.
61+
// See: node_modules/better-auth/dist/api/rate-limiter/index.mjs
62+
export const BETTER_AUTH_RATE_LIMIT = process.env.BETTER_AUTH_RATE_LIMIT
63+
? Number.parseInt(process.env.BETTER_AUTH_RATE_LIMIT, 10)
64+
: undefined;
65+
5366
// Trusted origins for Better Auth
5467
const trustedOriginsFromEnv = process.env.TRUSTED_ORIGINS
5568
? process.env.TRUSTED_ORIGINS.split(",").map((s) => s.trim())

tests/e2e/fixtures.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,4 @@ export const test = base.extend<{ authenticatedPage: Page }>({
2222
});
2323

2424
export { expect } from "@playwright/test";
25+
export { login };

tests/e2e/login.spec.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
1-
import { expect, test } from "./fixtures";
1+
import { expect, login, test } from "./fixtures";
22

33
test.describe("Login flow", () => {
44
test("sign in and land on Catalog", async ({ page }) => {
5-
await page.goto("/signin");
6-
await page.getByRole("button", { name: /oidc|okta/i }).click();
5+
await login(page);
76
await expect(page).toHaveURL(/\/catalog$/);
87
await expect(
98
page.getByRole("heading", { name: "MCP Server Catalog" }),

0 commit comments

Comments
 (0)