import { test, expect } from "@playwright/test"; const core = "http://localhost:17324"; const cookieName = "research_reader_session"; const account = page => page.locator(".account-name"); const logout = page => page.getByRole("button", { name: "退出登录", exact: true }); const loginLink = page => page.getByRole("link", { name: "登录", exact: true }); async function control(request, data) { await request.post(core + "/__test/control", { data }); } async function state(request) { return (await request.get(core + "/__test/state")).json(); } async function login(page) { await page.goto("/login"); await page.getByLabel("邮箱地址").fill("reader@example.test"); await page.getByLabel("密码", { exact: true }).fill("fixture-password-123"); await page.getByRole("button", { name: "登录", exact: true }).click(); await expect(account(page)).toHaveText("reader@example.test"); } test.beforeEach(async ({ request }) => { expect((await request.post(core + "/__test/reset")).ok()).toBe(true); }); test("HttpOnly session survives reload/new tabs and logout propagates", async ({ page, context, request }) => { const responsePromise = page.waitForResponse(r => r.url().endsWith("/api/foundry/auth/login")); await login(page); const response = await responsePromise; expect(await response.json()).not.toHaveProperty("access_token"); const cookie = (await context.cookies()).find(c => c.name === cookieName); expect(cookie).toMatchObject({ httpOnly: true, sameSite: "Lax", path: "/" }); expect(cookie.expires).toBeGreaterThan(Date.now() / 1000 + 29 * 86400); expect(await page.evaluate(() => document.cookie)).not.toContain(cookieName); expect(await page.evaluate(() => [localStorage.length, sessionStorage.length])).toEqual([0, 0]); await page.reload(); await expect(account(page)).toHaveText("reader@example.test"); const other = await context.newPage(); await other.goto("/"); await expect(account(other)).toHaveText("reader@example.test"); await logout(other).click(); await expect(loginLink(other)).toBeVisible(); await expect(loginLink(page)).toBeVisible(); expect((await context.cookies()).find(c => c.name === cookieName)).toBeUndefined(); expect((await state(request)).sessions).toBe(0); }); test("temporary Core failure preserves cookie and retry restores the account", async ({ page, context, request }) => { await login(page); await control(request, { meFailure: true }); await page.reload(); await expect(page.getByRole("button", { name: "重试账户" })).toBeVisible(); expect((await context.cookies()).find(c => c.name === cookieName)).toBeDefined(); await control(request, { meFailure: false }); await page.getByRole("button", { name: "重试账户" }).click(); await expect(account(page)).toHaveText("reader@example.test"); }); test("revoked session is cleared and cannot restore on reload", async ({ page, context, request }) => { await login(page); await control(request, { expire: true }); await page.reload(); await expect(loginLink(page)).toBeVisible(); expect((await context.cookies()).find(c => c.name === cookieName)).toBeUndefined(); await page.reload(); await expect(loginLink(page)).toBeVisible(); expect((await state(request)).sessions).toBe(0); }); test("failed logout retains the session until a successful retry", async ({ page, context, request }) => { await login(page); await control(request, { logoutFailure: true }); await logout(page).click(); await expect(page.getByText("退出失败,请重试", { exact: true })).toBeVisible(); await expect(account(page)).toHaveText("reader@example.test"); expect((await context.cookies()).find(c => c.name === cookieName)).toBeDefined(); await control(request, { logoutFailure: false }); await logout(page).click(); await expect(loginLink(page)).toBeVisible(); expect((await state(request)).logout).toBe(1); }); for (const provider of ["Google", "Slack"]) test(`${provider} synthetic PKCE callback exchanges exactly once`, async ({ page, context, request }) => { await page.goto("/login"); await page.getByRole("button", { name: provider, exact: true }).click(); await expect(account(page)).toHaveText("reader@example.test"); await expect(page).toHaveURL("http://localhost:17323/"); expect(await page.evaluate(() => sessionStorage.length)).toBe(0); const stats = await state(request); expect(stats.exchange).toBe(1); expect(stats[provider.toLowerCase()]).toBe(1); expect((await context.cookies()).find(c => c.name === cookieName)?.httpOnly).toBe(true); }); test("registration requests verification and callback consumes its token once", async ({ page, request }) => { await page.goto("/login"); await page.getByRole("button", { name: "创建账户", exact: true }).click(); await page.getByLabel("邮箱地址").fill("reader@example.test"); await page.getByLabel("密码", { exact: true }).fill("fixture-password-123"); await page.getByRole("button", { name: "创建账户", exact: true }).first().click(); await expect(page.getByRole("status")).toContainText("验证邮件已请求发送"); await expect(loginLink(page)).toBeVisible(); await page.goto("/auth/callback#verify_email=fixture-email-verification"); await expect(account(page)).toHaveText("reader@example.test"); await expect(page).toHaveURL("http://localhost:17323/"); expect((await state(request)).verified).toBe(1); });