后端:adminpanel 增 teams(列/详情/启停)+ users(列/启停/强制改密),AdminTeam/User 序列化器; login 校验团队 status,停用团队成员拒登;停用用户清 token、禁停平台超管;全程 IsPlatformAdmin + 审计。 前端:adminApi teams/users 系列;Admin 团队页(筛选+搜索+详情弹窗+启停)、用户页(启停+改密弹窗,超管行禁操作)。 测试:adminpanel 19 + accounts 22 = 41 单测过;无头 e2e _admin-p3.mjs 7 断言过 + 0 console error (UI 停用团队→成员登录 400→启用→200;UI 改密→新密码登录 200);tsc+build 绿。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
113 lines
5.5 KiB
JavaScript
113 lines
5.5 KiB
JavaScript
// Phase 3 e2e:平台团队 + 用户管理 —— 列表/详情/启停(团队停用→成员登录被拒)/强制改密。
|
|
// 用一次性 P3 团队做启停与改密,绝不动 demo 团队。headless + token 注入 + 真点击 + 0 console error。
|
|
import { chromium } from "playwright";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
const BASE = process.env.BASE || "http://127.0.0.1:5188";
|
|
const API = process.env.API || "http://127.0.0.1:8010";
|
|
const OUT = path.resolve("output/admin");
|
|
fs.mkdirSync(OUT, { recursive: true });
|
|
const stamp = Date.now();
|
|
const OWNER = `p3o-${stamp}`;
|
|
const TEAM = `P3Team${stamp}`;
|
|
|
|
async function apiLogin(u, p) {
|
|
const res = await fetch(`${API}/api/auth/login/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: u, password: p }) });
|
|
return { status: res.status, body: res.ok ? await res.json() : null };
|
|
}
|
|
|
|
const admin = (await apiLogin("admin", "admin123")).body;
|
|
// 一次性团队:超管发开团队码 → 注册 P3 owner 开团队
|
|
const inviteRes = await fetch(`${API}/api/admin/invitations/`, { method: "POST", headers: { "Content-Type": "application/json", Authorization: `Token ${admin.token}` }, body: JSON.stringify({}) });
|
|
const code = (await inviteRes.json()).code;
|
|
await fetch(`${API}/api/auth/register/`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username: OWNER, password: "ownerpass1", team_name: TEAM, invite_code: code }) });
|
|
|
|
const r = { teams: {}, toggle: {}, users: {}, reset: {}, consoleErrors: [], pass: false };
|
|
const browser = await chromium.launch({ headless: true });
|
|
const hook = (p, tag) => {
|
|
p.on("console", (m) => { if (m.type() === "error") r.consoleErrors.push(`${tag}:${m.text()}`); });
|
|
p.on("pageerror", (e) => r.consoleErrors.push(`${tag}:PAGEERR:${e.message}`));
|
|
};
|
|
|
|
const ctx = await browser.newContext({ viewport: { width: 1440, height: 900 } });
|
|
await ctx.addInitScript((tk) => localStorage.setItem("airshelf_token", tk), admin.token);
|
|
|
|
// ── 团队页 ──
|
|
{
|
|
const p = await ctx.newPage(); hook(p, "teams");
|
|
await p.goto(BASE + "/admin/teams", { waitUntil: "load" });
|
|
await p.waitForSelector(".admin-app", { timeout: 12000 });
|
|
await p.waitForSelector(".admin-table, .empty-state.show", { timeout: 8000 }).catch(() => {});
|
|
r.teams.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim();
|
|
await p.screenshot({ path: path.join(OUT, "p3-teams-list.png"), fullPage: true });
|
|
|
|
// 搜索我们的一次性团队
|
|
await p.fill(".admin-search", TEAM);
|
|
await p.waitForTimeout(800);
|
|
r.teams.foundOurTeam = (await p.locator(".admin-table tbody tr").count()) === 1;
|
|
|
|
// 详情弹窗
|
|
await p.locator(".admin-table tbody tr:first-child .btn-ghost").first().click();
|
|
await p.waitForSelector(".modal", { timeout: 6000 });
|
|
await p.waitForTimeout(400);
|
|
r.teams.detailMembers = await p.locator(".modal .admin-table tbody tr").count();
|
|
await p.screenshot({ path: path.join(OUT, "p3-team-detail.png") });
|
|
await p.locator(".modal-f .btn").click();
|
|
await p.waitForTimeout(300);
|
|
|
|
// 停用 → 成员登录应被拒
|
|
await p.locator(".admin-table tbody tr:first-child .btn-ghost.danger").click();
|
|
await p.waitForTimeout(900);
|
|
r.toggle.afterDisableLogin = (await apiLogin(OWNER, "ownerpass1")).status; // 期望 400
|
|
// 启用 → 恢复可登
|
|
await p.locator(".admin-table tbody tr:first-child .btn-ghost").last().click();
|
|
await p.waitForTimeout(900);
|
|
r.toggle.afterEnableLogin = (await apiLogin(OWNER, "ownerpass1")).status; // 期望 200
|
|
}
|
|
|
|
// ── 用户页 ──
|
|
{
|
|
const p = await ctx.newPage(); hook(p, "users");
|
|
await p.goto(BASE + "/admin/users", { waitUntil: "load" });
|
|
await p.waitForSelector(".admin-app", { timeout: 12000 });
|
|
await p.waitForSelector(".admin-table, .empty-state.show", { timeout: 8000 }).catch(() => {});
|
|
r.users.title = (await p.locator(".content h1").first().innerText().catch(() => "")).trim();
|
|
await p.screenshot({ path: path.join(OUT, "p3-users-list.png"), fullPage: true });
|
|
|
|
// admin 行应有「超管」标记且不可启停(搜 admin)
|
|
await p.fill(".admin-search", "admin");
|
|
await p.waitForTimeout(700);
|
|
r.users.adminHasBadge = (await p.locator(".admin-table tbody tr:first-child .admin-inline-pill").count()) >= 1;
|
|
|
|
// 改密一次性 owner
|
|
await p.fill(".admin-search", OWNER);
|
|
await p.waitForTimeout(700);
|
|
r.users.foundOwner = (await p.locator(".admin-table tbody tr").count()) === 1;
|
|
await p.locator(".admin-table tbody tr:first-child .btn-ghost").first().click(); // 改密
|
|
await p.waitForSelector(".modal", { timeout: 6000 });
|
|
await p.fill("#reset-pwd", "p3newpass1");
|
|
await p.locator(".modal-f .btn-primary").click();
|
|
await p.waitForTimeout(900);
|
|
r.reset.newPwdLogin = (await apiLogin(OWNER, "p3newpass1")).status; // 期望 200
|
|
await p.screenshot({ path: path.join(OUT, "p3-after-reset.png") });
|
|
}
|
|
|
|
await ctx.close();
|
|
await browser.close();
|
|
|
|
const checks = {
|
|
teamsPage: r.teams.title === "团队" && r.teams.foundOurTeam === true,
|
|
teamDetail: r.teams.detailMembers >= 1,
|
|
disableBlocksLogin: r.toggle.afterDisableLogin === 400,
|
|
enableRestoresLogin: r.toggle.afterEnableLogin === 200,
|
|
usersPage: r.users.title === "用户" && r.users.adminHasBadge === true && r.users.foundOwner === true,
|
|
resetPasswordWorks: r.reset.newPwdLogin === 200,
|
|
zeroConsoleErrors: r.consoleErrors.length === 0,
|
|
};
|
|
r.checks = checks;
|
|
r.pass = Object.values(checks).every(Boolean);
|
|
console.log(JSON.stringify(r, null, 2));
|
|
fs.writeFileSync(path.join(OUT, "p3-summary.json"), JSON.stringify(r, null, 2));
|
|
process.exit(r.pass ? 0 : 1);
|