Files
xiaobai-review/tests/e2e/login-portal.spec.js
T
f0a1adf52f 施工(HEL-226): 实现登录门户与本机免密切换账号
用设备 Cookie 和授权表记住本机已验证账号,登录页按确认样图做成门户,不再把密码写进浏览器。

Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: multica-agent <github@multica.ai>
2026-08-29 11:24:39 +08:00

152 lines
5.5 KiB
JavaScript

const { test, expect } = require("@playwright/test");
function loginPayload(user) {
return {
ok: true,
authenticated: true,
csrf_token: "portal-csrf",
user,
};
}
async function mockLoginPortal(page, options = {}) {
const accounts = options.accounts || [];
let currentUserId = options.currentUserId ?? null;
await page.route("**/api/**", async (route) => {
const url = new URL(route.request().url());
const method = route.request().method();
if (url.pathname === "/api/auth/accounts") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true, accounts, current_user_id: currentUserId }),
});
return;
}
if (url.pathname === "/api/auth/switch" && method === "POST") {
const body = route.request().postDataJSON() || {};
const account = accounts.find((item) => Number(item.user_id) === Number(body.user_id));
if (!account || options.switchFails) {
await route.fulfill({
status: 401,
contentType: "application/json",
body: JSON.stringify({ error: "该账号需重新验证" }),
});
return;
}
currentUserId = account.user_id;
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(loginPayload(account)),
});
return;
}
if (url.pathname === "/api/auth/forget" && method === "POST") {
const body = route.request().postDataJSON() || {};
const index = accounts.findIndex((item) => Number(item.user_id) === Number(body.user_id));
if (index >= 0) accounts.splice(index, 1);
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
return;
}
if ((url.pathname === "/api/auth/login" || url.pathname === "/api/auth/register") && method === "POST") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify(loginPayload({
id: 9,
username: "new_user",
role: "user",
membership: { active: false, subscribed: false, is_admin: false },
})),
});
return;
}
if (url.pathname === "/api/auth/me") {
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({
ok: true,
authenticated: Boolean(currentUserId),
csrf_token: "portal-csrf",
user: accounts.find((item) => Number(item.user_id) === Number(currentUserId)) || null,
}),
});
return;
}
await route.fulfill({
status: 200,
contentType: "application/json",
body: JSON.stringify({ ok: true }),
});
});
}
const SAVED_ACCOUNTS = [
{
user_id: 1,
username: "alpha_user",
role: "admin",
membership: { active: true, subscribed: true, is_admin: true },
last_used_at: "2026-08-29T01:00:00+00:00",
},
{
user_id: 2,
username: "beta_user",
role: "user",
membership: { active: false, subscribed: false, is_admin: false },
last_used_at: "2026-08-28T01:00:00+00:00",
},
];
test("first-time login portal asks for a password and hides environment copy", async ({ page }) => {
await mockLoginPortal(page, { accounts: [] });
await page.goto("/login/");
await expect(page.locator(".login-card-title")).toHaveText("欢迎回来");
await expect(page.locator("#loginUsername")).toBeVisible();
await expect(page.locator(".login-submit")).toHaveText("登录");
await expect(page.locator("body")).not.toContainText("内网个人版");
await expect(page.locator("body")).not.toContainText("192.168.200.11");
});
test("saved accounts can switch directly and show a re-auth message on failure", async ({ page }) => {
await mockLoginPortal(page, { accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })) });
await page.goto("/login/");
await expect(page.locator(".login-card-title")).toHaveText("选择账号");
await expect(page.locator(".login-account-row")).toHaveCount(2);
const switched = page.waitForRequest((request) => (
request.url().includes("/api/auth/switch") && request.method() === "POST"
));
await page.locator('[data-switch-id="2"]').click();
const request = await switched;
expect(JSON.parse(request.postData() || "{}")).toEqual({ user_id: 2 });
});
test("failed account switch stays on the portal with the original copy", async ({ page }) => {
await mockLoginPortal(page, {
accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })),
switchFails: true,
});
await page.goto("/login/");
await page.locator('[data-switch-id="2"]').click();
await expect(page.locator(".login-error")).toHaveText("该账号需重新验证");
await expect(page).toHaveURL(/\/login\/?/);
});
test("managing accounts removes a local record after inline confirmation", async ({ page }) => {
await mockLoginPortal(page, { accounts: SAVED_ACCOUNTS.map((item) => ({ ...item })) });
await page.goto("/login/");
await page.locator('[data-login-action="manage"]').click();
await expect(page.locator(".login-card-title")).toHaveText("管理账号记录");
await page.locator('[data-confirm-id="2"]').click();
await expect(page.locator(".login-confirm-copy")).toContainText("beta_user");
await page.locator('[data-forget-id="2"]').click();
await expect(page.locator(".login-account-row")).toHaveCount(1);
await expect(page.locator(".login-account-row")).toContainText("alpha_user");
});