用设备 Cookie 和授权表记住本机已验证账号,登录页按确认样图做成门户,不再把密码写进浏览器。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
152 lines
5.5 KiB
JavaScript
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");
|
|
});
|