|
| 1 | +import { test, expect } from "@playwright/test"; |
| 2 | +import { AUTH_DEFAULT_PORT, PLANNER_DEFAULT_PORT } from "../src/config.js"; |
| 3 | + |
| 4 | +const authBase = `http://localhost:${AUTH_DEFAULT_PORT}`; |
| 5 | + |
| 6 | +test("OAuth 2.1 magic link flow", async ({ page, request }) => { |
| 7 | + const email = `e2e-${Date.now()}@example.com`; |
| 8 | + |
| 9 | + // Clear any stale magic links |
| 10 | + await request.delete(`${authBase}/api/test/magic-links`); |
| 11 | + |
| 12 | + // Start OAuth flow: unauthenticated user hits authorize endpoint with PKCE |
| 13 | + const codeChallenge = "MJk6-W6P2z_PgOvWcEvbyqeIyc-GthZov8-QX37r0Vo"; |
| 14 | + const authorizeParams = new URLSearchParams({ |
| 15 | + client_id: "planner", |
| 16 | + redirect_uri: `http://localhost:${PLANNER_DEFAULT_PORT}/auth/codebar/callback`, |
| 17 | + response_type: "code", |
| 18 | + state: "e2e-state-123", |
| 19 | + scope: "openid profile", |
| 20 | + code_challenge: codeChallenge, |
| 21 | + code_challenge_method: "S256", |
| 22 | + }); |
| 23 | + await page.goto( |
| 24 | + `${authBase}/api/auth/oauth2/authorize?${authorizeParams.toString()}`, |
| 25 | + ); |
| 26 | + |
| 27 | + // OAuth provider redirects unauthenticated users to the login page |
| 28 | + await expect(page).toHaveURL(/\/login/); |
| 29 | + |
| 30 | + // Click through to the magic link form |
| 31 | + await page.getByRole("button", { name: /Send Magic Link/i }).click(); |
| 32 | + await expect(page).toHaveURL(/\/login\/magic-link/); |
| 33 | + |
| 34 | + // Submit email |
| 35 | + await page.fill('input[name="email"]', email); |
| 36 | + await page.click('button[type="submit"]'); |
| 37 | + |
| 38 | + // Success page is shown |
| 39 | + await expect(page).toHaveURL(/\/login\/magic-link\?success=/); |
| 40 | + await expect(page.locator("body")).toContainText("Magic link sent"); |
| 41 | + |
| 42 | + // Fetch the magic link from the auth dev endpoint |
| 43 | + const linksRes = await request.get(`${authBase}/api/test/magic-links`); |
| 44 | + const links = await linksRes.json(); |
| 45 | + const link = links.find((l) => l.email === email); |
| 46 | + expect(link).toBeDefined(); |
| 47 | + expect(link.url).toMatch(/magic-link\/verify/); |
| 48 | + |
| 49 | + // Call the magic link verify endpoint via API to establish a session. |
| 50 | + // APIRequestContext automatically stores and sends cookies across requests. |
| 51 | + const verifyRes = await request.get(link.url, { maxRedirects: 0 }); |
| 52 | + expect(verifyRes.status()).toBe(302); |
| 53 | + const verifyLocation = verifyRes.headers()["location"]; |
| 54 | + expect(verifyLocation).toContain("/api/auth/oauth2/authorize"); |
| 55 | + |
| 56 | + // Call the authorize endpoint with the session cookie (don't follow redirects) |
| 57 | + const authRes = await request.get( |
| 58 | + `${authBase}/api/auth/oauth2/authorize?${authorizeParams.toString()}`, |
| 59 | + { maxRedirects: 0 }, |
| 60 | + ); |
| 61 | + |
| 62 | + // Should get a 302 redirect to the planner callback with the code |
| 63 | + expect(authRes.status()).toBe(302); |
| 64 | + const location = authRes.headers()["location"]; |
| 65 | + expect(location).toBeTruthy(); |
| 66 | + expect(location).toContain("code="); |
| 67 | + expect(location).toContain("state=e2e-state-123"); |
| 68 | +}); |
0 commit comments