/** 登录结果的会话归属:真实页面、真实跨标签 storage 事件和受控外部 HTTP 响应。 */ import assert from "node:assert/strict"; import { test } from "node:test"; import { appOrigin, backendOrigin, eventually, login, scenario, } from "./helpers/console-browser.mjs"; function deferred() { let resolve; const promise = new Promise((done) => (resolve = done)); return { promise, resolve }; } async function respond(route, data, status = 200, msg = "") { return route.fulfill({ json: { status, msg, data }, headers: { "Access-Control-Allow-Origin": "*" }, }); } async function oauthBoundary(page, handle) { // 只声明 OAuth 的外部协议;普通登录、账户和列表仍使用独立 HTTP 假后端。 await page .context() .route(`${backendOrigin}/console/v1/**`, async (route) => { const path = new URL(route.request().url()).pathname.replace( /^\/console\/v1/, "", ); if (await handle(route, path)) return; // 基线复现若错误接纳旧会话,让该外部账号的真实首页继续加载,不以 401 掩盖覆盖。 const previousAccount = { "/index": { account: { id: 99, username: "older-oauth-account", credit: "0.00" }, }, "/menu": { menu: [] }, "/client_care/mail": { credit_limit: { count: 0, list: [], type: [] } }, "/certification/info": {}, "/host": { list: [], count: 0 }, "/ticket": { list: [], count: 0 }, "/ticket/status": { list: [] }, "/ticket/statistic": {}, "/announcement": { list: [], count: 0 }, "/recommend/promoter": {}, "/transaction": { list: [], count: 0 }, "/withdraw/rule/credit": {}, "/ssh_key": { list: [], count: 0 }, }; if ( route.request().headers().authorization === "Bearer test-older-oauth-session" && Object.hasOwn(previousAccount, path) ) return respond(route, previousAccount[path]); return route.fallback(); }); } async function changeSession(page, transition) { // 不预置 token,也不手工派发 storage;另一个真实标签完成登录和可选的退出。 await page.evaluate(() => { window.oauthSessionTransitions = []; window.addEventListener("storage", (event) => { if (event.key === "jwt") window.oauthSessionTransitions.push( event.newValue ? "login" : "logout", ); }); }); const other = await page.context().newPage(); await login(other); await other.getByText("audit-01", { exact: true }).waitFor(); await eventually( () => page.evaluate(() => window.oauthSessionTransitions), (events) => events.includes("login"), "New login must synchronize before the pending OAuth result is released", ); if (transition === "logout") { await other.getByRole("button", { name: "用户菜单", exact: true }).click(); await other .getByRole("menuitem", { name: "退出登录", exact: true }) .click(); await other.waitForURL((url) => url.pathname === "/login"); await eventually( () => page.evaluate(() => window.oauthSessionTransitions), (events) => events.includes("logout"), "Logout must synchronize before the pending OAuth result is released", ); } return other; } async function sessionIsPreserved(page, transition) { assert.equal( await page.evaluate( (loggedOut) => localStorage.getItem("jwt") === (loggedOut ? null : "e2e-session-token"), transition === "logout", ), true, "An older result must preserve the session established by the newer action", ); } test( "OAuth callback establishes a current session normally", { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { await oauthBoundary(page, async (route, path) => { if (path !== "/oauth/token") return false; await respond(route, { jwt: "e2e-session-token" }); return true; }); await page.goto(`${appOrigin}/oauth/callback`); await page.waitForURL((url) => url.pathname === "/"); await page .getByRole("button", { name: "用户菜单", exact: true }) .waitFor(); await sessionIsPreserved(page, "login"); return { currentOAuthSessionAccepted: true, memberHomeVisible: true }; }), ); test( "OAuth verified binding establishes a current session normally", { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { await oauthBoundary(page, async (route, path) => { if (path === "/oauth/token") { await respond(route, { url: "/oauth/bind" }); return true; } if (path === "/oauth/client/bind") { await respond(route, { jwt: "e2e-session-token" }); return true; } return false; }); await page.goto(`${appOrigin}/oauth/bind?redirect=/security/ssh-keys`); await page.getByLabel("手机号", { exact: true }).fill("13800138000"); await page.getByLabel("验证码", { exact: true }).fill("123456"); await page.getByRole("checkbox").check(); await page.getByRole("button", { name: "确认绑定", exact: true }).click(); await page.waitForURL((url) => url.pathname === "/security/ssh-keys"); await page.getByText("audit-01", { exact: true }).waitFor(); await sessionIsPreserved(page, "login"); return { verifiedBindingAccepted: true, protectedPageVisible: true }; }), ); for (const pagePath of ["/oauth/callback", "/oauth/bind"]) { for (const transition of ["login", "logout"]) { for (const outcome of ["jwt", "bind", "error"]) { test( `${pagePath} ignores older ${outcome} after cross-tab ${transition}`, { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); await oauthBoundary(page, async (route, path) => { if (path !== "/oauth/token") return false; started.resolve(); await release.promise; await respond( route, outcome === "jwt" ? { jwt: "test-older-oauth-session" } : { url: "/oauth/bind" }, outcome === "error" ? 422 : 200, outcome === "error" ? "过期授权请求失败" : "", ); return true; }); await page.goto(`${appOrigin}${pagePath}`); await started.promise; await changeSession(page, transition); release.resolve(); const retry = page.getByRole("button", { name: pagePath === "/oauth/callback" ? "重新验证" : "验证授权会话", exact: true, }); await eventually( () => retry.isEnabled().catch(() => false), Boolean, "The old request must finish without navigating or enabling its old binding flow", ); assert.equal(new URL(page.url()).pathname, pagePath); assert.equal(await page.getByRole("alert").count(), 0); assert.equal( await page.getByLabel("手机号", { exact: true }).count(), 0, ); await sessionIsPreserved(page, transition); return { transition, outcome, newerSessionPreserved: true, noOldNavigation: true, noOldError: true, noOldBindingForm: true, }; }), ); } } } for (const transition of ["login", "logout"]) { test( `OAuth binding submission ignores its late session after cross-tab ${transition}`, { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); await oauthBoundary(page, async (route, path) => { if (path === "/oauth/token") { await respond(route, { url: "/oauth/bind" }); return true; } if (path === "/oauth/client/bind") { started.resolve(); await release.promise; await respond(route, { jwt: "test-older-oauth-session" }); return true; } return false; }); await page.goto(`${appOrigin}/oauth/bind?redirect=/security/ssh-keys`); await page.getByLabel("手机号", { exact: true }).fill("13800138000"); await page.getByLabel("验证码", { exact: true }).fill("123456"); await page.getByRole("checkbox").check(); const submit = page.getByRole("button", { name: "确认绑定", exact: true, }); await submit.click(); await started.promise; await changeSession(page, transition); release.resolve(); await eventually( () => submit.isEnabled().catch(() => false), Boolean, "The old binding must finish without replacing the newer session", ); assert.equal(new URL(page.url()).pathname, "/oauth/bind"); await sessionIsPreserved(page, transition); return { transition, lateBindingIgnored: true, newerSessionPreserved: true, }; }), ); } async function wechatBoundary(page, statusHandler) { await oauthBoundary(page, async (route, path) => { if (path === "/common") { await respond(route, { website_name: "本地扫码验收", login_phone_password: 1, login_email_password: 1, login_phone_verify: 1, register_phone: 1, register_email: 1, plugin_configuration: { mp_weixin_notice: { scan_login: 1 } }, oauth: [], }); return true; } if (path === "/mp_weixin_notice/qrcode") { await respond(route, { img_url: "data:image/gif;base64,R0lGODlhAQABAIAAAP///wAAACH5BAEAAAAALAAAAAABAAEAAAICRAEAOw==", ticket: "local-qr-ticket", token: "local-qr-token", }); return true; } return statusHandler(route, path); }); } async function requestWechat(page) { await page.goto(`${appOrigin}/login?redirect=/security/ssh-keys`); await page.getByRole("button", { name: "获取二维码", exact: true }).click(); await page.getByRole("img", { name: "微信扫码登录", exact: true }).waitFor(); } test( "a current WeChat scan establishes its session normally", { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { await wechatBoundary(page, async (route, path) => { if (path !== "/mp_weixin_notice/qrcode/status") return false; await respond(route, { status: "Success", jwt: "e2e-session-token" }); return true; }); await requestWechat(page); await page.waitForURL((url) => url.pathname === "/security/ssh-keys"); await page.getByText("audit-01", { exact: true }).waitFor(); await sessionIsPreserved(page, "login"); return { currentWechatSessionAccepted: true }; }), ); test( "a current WeChat account selection establishes its session normally", { timeout: 25000 }, (t) => scenario(t, {}, async (page) => { await wechatBoundary(page, async (route, path) => { if (path === "/mp_weixin_notice/qrcode/status") { await respond(route, { status: "SelectClient", client: [{ id: 1, username: "current-qr-account", status: 1 }], }); return true; } if (path === "/mp_weixin_notice/qrcode/select_client_login") { await respond(route, { jwt: "e2e-session-token" }); return true; } return false; }); await requestWechat(page); await page.locator(".qr-client-btn").click(); await page.waitForURL((url) => url.pathname === "/security/ssh-keys"); await page.getByText("audit-01", { exact: true }).waitFor(); await sessionIsPreserved(page, "login"); return { currentWechatSelectionAccepted: true }; }), ); for (const outcome of ["Success", "WaitBind", "SelectClient"]) { test( `WeChat ${outcome} result cannot resume its old scan after a newer session`, { timeout: 30000 }, (t) => scenario(t, {}, async (page) => { const started = deferred(); const release = deferred(); const transition = outcome === "SelectClient" ? "logout" : "login"; await wechatBoundary(page, async (route, path) => { if (path === "/mp_weixin_notice/qrcode/status") { if (outcome === "SelectClient") { await respond(route, { status: "SelectClient", client: [{ id: 99, username: "older-qr-account", status: 1 }], }); } else { started.resolve(); await release.promise; await respond(route, { status: outcome, ...(outcome === "Success" ? { jwt: "test-older-oauth-session" } : {}), }); } return true; } if (path === "/mp_weixin_notice/qrcode/select_client_login") { started.resolve(); await release.promise; await respond(route, { jwt: "test-older-oauth-session" }); return true; } return false; }); await requestWechat(page); if (outcome === "SelectClient") await page.locator(".qr-client-btn").click(); await started.promise; await changeSession(page, transition); const completed = page.waitForResponse((response) => response .url() .endsWith( outcome === "SelectClient" ? "/mp_weixin_notice/qrcode/select_client_login" : "/mp_weixin_notice/qrcode/status?ticket=local-qr-ticket&token=local-qr-token", ), ); release.resolve(); await (await completed).finished(); // 网络完成后跨过一次实际渲染,观察迟到结果,而非在释放响应前就读取已清空的二维码。 await page.evaluate( () => new Promise((done) => requestAnimationFrame(() => requestAnimationFrame(done)), ), ); await page .getByRole("button", { name: "获取二维码", exact: true }) .waitFor(); assert.equal(new URL(page.url()).pathname, "/login"); assert.equal(await page.locator(".qr-client-btn").count(), 0); assert.equal( await page.evaluate( () => sessionStorage.getItem("console.wechat-binding") === null, ), true, ); await sessionIsPreserved(page, transition); return { outcome, transition, oldScanDiscarded: true, newerSessionPreserved: true, }; }), ); }