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"); });