test: 用户管理全生命周期测试(42项)覆盖异常case

覆盖场景:
- 创建用户异常:重复用户名、密码太短、空字段
- 权限边界:USER 不能创建/查看/删除用户
- 角色变更:USER↔TENANT_ADMIN 切换后权限实时生效
- 删除异常:删自己、删 admin、删不存在用户
- UI 验证:角色列、编辑弹窗、权限管理页、权限矩阵

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Developer
2026-06-09 09:00:23 +08:00
parent 9b4412792b
commit 64771f10ed
+401
View File
@@ -0,0 +1,401 @@
/**
* 用户管理全生命周期测试
*
* 覆盖场景:
* - 三种角色(SUPER_ADMIN / TENANT_ADMIN / USER)的 CRUD 权限
* - 创建用户的各种异常 case(重复用户名、密码太短、空字段)
* - 编辑用户(改名、改角色)
* - 删除用户(删自己、删 admin、删不存在的人)
* - 角色变更后权限实时生效
* - UI 交互验证(Playwright
*/
import { chromium } from 'playwright';
const API = 'http://localhost:3001';
const BASE = 'http://localhost:13001';
const TENANT_ID = 'a140a68e-f70a-44d3-b753-fa33d48cf234';
// ── 工具函数 ──
let pass = 0, fail = 0;
function assert(label, ok, detail = '') {
if (ok) {
console.log(`${label}`);
pass++;
} else {
console.log(`${label}${detail ? ' — ' + detail : ''}`);
fail++;
}
}
async function loginApi(username, password) {
const r = await fetch(`${API}/api/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!r.ok) return null;
const data = await r.json();
return data.access_token;
}
async function api(token, method, path, body = null) {
const opts = {
method,
headers: { 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json' },
};
if (body) opts.body = JSON.stringify(body);
const r = await fetch(`${API}/api${path}`, opts);
const status = r.status;
let data = null;
try { data = await r.json(); } catch { data = null; }
return { status, data };
}
/** 通过 Playwright 登录并获取 apiKey */
async function getApiKey(page, username, password) {
await page.goto(`${BASE}/login`, { waitUntil: 'networkidle' });
await page.waitForTimeout(1000);
await page.locator('input[type="text"]').first().fill(username);
await page.locator('input[type="password"]').first().fill(password);
await page.locator('button[type="submit"]').click();
await page.waitForURL('**/');
await page.waitForTimeout(500);
return await page.evaluate(() => localStorage.getItem('kb_api_key') || '');
}
// ── 主测试 ──
async function run() {
const browser = await chromium.launch({ headless: true });
console.log('\n' + '='.repeat(70));
console.log('🧪 用户管理全生命周期测试');
console.log('='.repeat(70));
// ──────────────────────────────────
// Phase 1: Admin 登录 + 创建测试用户
// ──────────────────────────────────
console.log('\n📦 Phase 1: 环境准备');
const adminToken = await loginApi('admin', 'admin123');
assert('admin 登录', !!adminToken);
// 创建 test-user-a(正常用户)
const r1 = await api(adminToken, 'POST', '/users', {
username: 'e2e-user-a', password: 'pass123', displayName: '测试用户A',
});
const userAId = r1.data?.user?.id || r1.data?.id;
assert('创建 userA', r1.status === 201 || r1.status === 200, `status=${r1.status}`);
assert(' userA 有 ID', !!userAId);
// 加到租户
const r1m = await api(adminToken, 'POST', `/v1/tenants/${TENANT_ID}/members`, {
userId: userAId, role: 'USER',
});
assert(' userA 加入租户', r1m.status === 201 || r1m.status === 200, `status=${r1m.status}`);
// 创建 test-user-b(后来会升为 TENANT_ADMIN
const r2 = await api(adminToken, 'POST', '/users', {
username: 'e2e-user-b', password: 'pass456', displayName: '测试用户B',
});
const userBId = r2.data?.user?.id || r2.data?.id;
assert('创建 userB', !!userBId);
const r2m = await api(adminToken, 'POST', `/v1/tenants/${TENANT_ID}/members`, {
userId: userBId, role: 'USER',
});
assert(' userB 加入租户', r2m.status === 201 || r2m.status === 200);
// ──────────────────────────────────
// Phase 2: 创建用户的异常情况
// ──────────────────────────────────
console.log('\n📦 Phase 2: 创建用户 — 异常 case');
// 2a. 重复用户名
const rDup = await api(adminToken, 'POST', '/users', {
username: 'e2e-user-a', password: 'pass123', displayName: '重复用户',
});
assert(' 重复用户名拒绝', rDup.status >= 400, `status=${rDup.status}`);
// 2b. 密码太短
const rShort = await api(adminToken, 'POST', '/users', {
username: 'e2e-user-short', password: '12', displayName: '短密码',
});
assert(' 密码太短拒绝', rShort.status >= 400, `status=${rShort.status}`);
// 2c. 空用户名
const rEmpty = await api(adminToken, 'POST', '/users', {
username: '', password: 'pass123', displayName: '空用户名',
});
assert(' 空用户名拒绝', rEmpty.status >= 400, `status=${rEmpty.status}`);
// 2d. 不传密码
const rNoPass = await api(adminToken, 'POST', '/users', {
username: 'e2e-user-nopass', displayName: '无密码',
});
assert(' 无密码拒绝', rNoPass.status >= 400, `status=${rNoPass.status}`);
// ──────────────────────────────────
// Phase 3: USER 角色不能创建用户
// ──────────────────────────────────
console.log('\n📦 Phase 3: 权限边界 — USER 不能创建/删除用户');
const userAToken = await loginApi('e2e-user-a', 'pass123');
assert(' userA 登录', !!userAToken);
const rForbidCreate = await api(userAToken, 'POST', '/users', {
username: 'e2e-user-forbid', password: 'pass123',
});
assert(' USER 创建用户被拒', rForbidCreate.status === 403, `got ${rForbidCreate.status}`);
const rForbidList = await api(userAToken, 'GET', '/users');
assert(' USER 查看用户列表被拒', rForbidList.status === 403, `got ${rForbidList.status}`);
const rForbidDelete = await api(userAToken, 'DELETE', `/users/${userBId}`);
assert(' USER 删除用户被拒', rForbidDelete.status === 403, `got ${rForbidDelete.status}`);
// ──────────────────────────────────
// Phase 4: 编辑用户 + 角色变更
// ──────────────────────────────────
console.log('\n📦 Phase 4: 编辑用户 & 角色变更');
// 4a. 改名
const rRename = await api(adminToken, 'PUT', `/users/${userAId}`, {
displayName: '用户A已改名',
});
assert(' 编辑用户名', rRename.status === 200, `got ${rRename.status}`);
// 4b. 提升为 TENANT_ADMIN
const rPromote = await api(adminToken, 'PATCH', `/v1/tenants/${TENANT_ID}/members/${userAId}`, {
role: 'TENANT_ADMIN',
});
assert(' 提升 userA 为管理员', rPromote.status === 200, `got ${rPromote.status}`);
// 4c. 验证权限实时生效
const userA2Token = await loginApi('e2e-user-a', 'pass123');
assert(' userA 重新登录', !!userA2Token);
const rCheckPerm = await fetch(`${API}/api/permissions/mine`, {
headers: { 'Authorization': `Bearer ${userA2Token}` },
});
const permData = await rCheckPerm.json();
const permCount = (permData.permissions || []).length;
assert(` userA 权限从 5→${permCount}`, permCount >= 20, `实际=${permCount}`);
// 4d. 验证现在可以查看用户列表了
const rCanList = await api(userA2Token, 'GET', '/users');
assert(' userA(TENANT_ADMIN) 能查看用户列表', rCanList.status === 200);
// 4e. 降回 USER
const rDemote = await api(adminToken, 'PATCH', `/v1/tenants/${TENANT_ID}/members/${userAId}`, {
role: 'USER',
});
assert(' 降回 userA 为 USER', rDemote.status === 200);
const userA3Token = await loginApi('e2e-user-a', 'pass123');
const rCheckPerm2 = await fetch(`${API}/api/permissions/mine`, {
headers: { 'Authorization': `Bearer ${userA3Token}` },
});
const permData2 = await rCheckPerm2.json();
const permCount2 = (permData2.permissions || []).length;
assert(` userA 权限从 ${permCount}${permCount2}`, permCount2 <= 5, `实际=${permCount2}`);
// ──────────────────────────────────
// Phase 5: 删除用户的异常情况
// ──────────────────────────────────
console.log('\n📦 Phase 5: 删除用户 — 异常 case');
// 5a. 删自己
// 先获取 admin 自己的 ID
const adminProfile = await api(adminToken, 'GET', '/users/me');
const adminId = adminProfile.data?.id;
assert(' admin profile 有 ID', !!adminId, `data=${JSON.stringify(adminProfile.data)}`);
if (adminId) {
const rSelf = await api(adminToken, 'DELETE', `/users/${adminId}`);
assert(' 不能删自己', rSelf.status >= 400, `got ${rSelf.status} msg=${JSON.stringify(rSelf.data)}`);
// 验证 admin 还在——通过 users 列表
const rCheckList = await api(adminToken, 'GET', '/users');
const allUsersAfter = Array.isArray(rCheckList.data) ? rCheckList.data : (rCheckList.data?.data || []);
const adminStillThere = allUsersAfter.some(u => u.id === adminId);
assert(' admin 还在', adminStillThere, `列表中无 admin ID`);
}
// 5b. 删不存在的用户
const rNonExist = await api(adminToken, 'DELETE', '/users/non-existent-id');
assert(' 删不存在用户返回 404', rNonExist.status === 404, `got ${rNonExist.status}`);
// 5c. 删 admin 账户
// 先查 admin 的 ID
const usersList = await api(adminToken, 'GET', '/users');
const allUsers = Array.isArray(usersList.data) ? usersList.data : (usersList.data?.data || []);
const realAdmin = allUsers.find(u => u.username === 'admin');
if (realAdmin) {
const rDelAdmin = await api(adminToken, 'DELETE', `/users/${realAdmin.id}`);
assert(' 不能删 admin 账号', rDelAdmin.status >= 400, `got ${rDelAdmin.status} msg=${JSON.stringify(rDelAdmin.data)}`);
}
// 5d. TENANT_ADMIN 删其他租户的用户(如果有的话)
// 创建另一个租户的用户
const rOtherTenant = await api(adminToken, 'POST', '/v1/tenants', { name: 'temp-other-tenant' });
const otherTenantId = rOtherTenant.data?.id;
if (otherTenantId) {
// 删除临时租户
await api(adminToken, 'DELETE', `/v1/tenants/${otherTenantId}`);
}
// ──────────────────────────────────
// Phase 6: 正常删除用户
// ──────────────────────────────────
console.log('\n📦 Phase 6: 正常删除用户(清理测试数据)');
const rDelA = await api(adminToken, 'DELETE', `/users/${userAId}`);
assert(' 删除 userA', rDelA.status === 200, `got ${rDelA.status}`);
const rCheckA = await api(adminToken, 'GET', `/users/${userAId}`);
assert(' userA 已不存在', rCheckA.status === 404, `got ${rCheckA.status}`);
const rDelB = await api(adminToken, 'DELETE', `/users/${userBId}`);
assert(' 删除 userB', rDelB.status === 200, `got ${rDelB.status}`);
// 验证删除后登录失败
const rLoginDel = await loginApi('e2e-user-a', 'pass123');
assert(' 删除后 userA 无法登录', !rLoginDel, `token=${!!rLoginDel}`);
// ──────────────────────────────────
// Phase 7: UI 验证
// ──────────────────────────────────
console.log('\n📦 Phase 7: UI 交互验证');
const page = await browser.newPage({ viewport: { width: 1440, height: 900 } });
// 7a. 登录 admin
const apiKey = await getApiKey(page, 'admin', 'admin123');
assert(' UI 登录成功', !!apiKey);
// 7b. 进入设置 → 点击「用户管理」侧栏按钮
await page.goto(`${BASE}/settings`, { waitUntil: 'networkidle' });
await page.waitForTimeout(2000);
const sidebarBtns = await page.evaluate(() => {
// 侧栏在 class 包含 w-64 和 bg-slate-50 的 div 里
const aside = document.querySelector('[class*="w-64"]') || document.querySelector('aside');
if (!aside) return [];
return Array.from(aside.querySelectorAll('button')).map(b => (b.textContent || '').trim()).filter(Boolean);
});
assert(' 设置页侧栏有按钮', sidebarBtns.length > 0, `按钮: ${sidebarBtns.slice(0,5).join(', ')}`);
// 点击用户管理
const userMgmtBtn = page.locator('button:has-text("用户管理")');
if (await userMgmtBtn.isVisible().catch(() => false)) {
await userMgmtBtn.click();
await page.waitForTimeout(2000);
assert(' 用户管理 Tab 可点击', true);
} else {
assert(' 用户管理按钮可见', false, '侧栏无"用户管理"');
}
// 7c. 检查用户表
const tables = await page.evaluate(() => document.querySelectorAll('table').length);
assert(' 用户表存在', tables > 0, `找到 ${tables} 个 table`);
const headers = await page.evaluate(() => {
return Array.from(document.querySelectorAll('th')).map(th => th.textContent?.trim());
});
assert(' 用户表有角色列', headers.some(h => h?.includes('角色')), `列: ${headers.join(', ')}`);
const rowCount = await page.evaluate(() => document.querySelectorAll('tbody tr').length);
assert(' 用户表有数据行', rowCount > 0, `${rowCount}`);
// 7d. 编辑用户弹窗 — 打开(找任意行的第一个操作按钮)
// 操作栏中编辑按钮在第1个
const firstActionBtn = page.locator('tbody tr button').first();
if (await firstActionBtn.isVisible().catch(() => false)) {
await firstActionBtn.click();
await page.waitForTimeout(1500);
// 检查弹窗中是否有角色选择按钮
const roleBtns = await page.evaluate(() => {
return Array.from(document.querySelectorAll('.fixed button, [class*="fixed"] button, [class*="inset-0"] button'))
.map(b => b.textContent?.trim())
.filter(t => t === '用户' || t === '管理员' || t === '超级管理员');
});
assert(' 编辑弹窗有角色选项', roleBtns.length >= 2, `找到 ${roleBtns.length}`);
// 检查是否有权限预览
const permPreview = await page.evaluate(() => {
return document.body.textContent?.includes('该角色的权限');
});
assert(' 编辑弹窗有权限预览', !!permPreview, '未找到"该角色的权限"');
// 关闭弹窗——点右上角 X 或点取消
const cancelBtn = page.locator('button:has-text("取消")').last();
if (await cancelBtn.isVisible().catch(() => false)) {
await cancelBtn.click();
}
// 等弹窗完全消失
await page.waitForTimeout(2000);
await page.waitForFunction(() => !document.querySelector('[class*="inset-0"][class*="z-\\[1000\\]"]'), { timeout: 5000 }).catch(() => {});
} else {
assert(' 操作按钮可见', false, '未找到任何行内操作按钮');
}
// 7e. 权限管理 Tab
// 先确保没有弹窗遮挡
await page.waitForFunction(() => !document.querySelector('.fixed.inset-0'), { timeout: 5000 }).catch(() => {});
await page.waitForTimeout(1500);
// 用 evaluate 直接点击,绕过任何 DOM 遮挡
const clicked = await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('button'));
const permBtn = btns.find(b => (b.textContent || '').includes('权限管理'));
if (permBtn) { permBtn.scrollIntoView({ block: 'center' }); permBtn.click(); return true; }
return false;
});
assert(' 权限管理按钮可点击', clicked);
await page.waitForTimeout(2000);
// 检查是否三个系统角色渲染
const hasRoles = await page.evaluate(() => {
const body = document.body.textContent || '';
return body.includes('SUPER_ADMIN') && body.includes('TENANT_ADMIN') && body.includes('USER');
});
assert(' 权限管理页显示三个系统角色', !!hasRoles);
// 点击 SUPER_ADMIN 角色查看权限
const superClicked = await page.evaluate(() => {
const btns = Array.from(document.querySelectorAll('button'));
const superBtn = btns.find(b => (b.textContent || '').includes('SUPER_ADMIN'));
if (superBtn) { superBtn.scrollIntoView({ block: 'center' }); superBtn.click(); return true; }
return false;
});
assert(' SUPER_ADMIN 角色可点击', superClicked);
await page.waitForTimeout(1500);
const permMatrix = await page.evaluate(() => {
const body = document.body.textContent || '';
return body.includes('用户管理') && body.includes('知识库');
});
assert(' 权限矩阵渲染', !!permMatrix);
await page.close();
// ──────────────────────────────────
// 汇总
// ──────────────────────────────────
console.log('\n' + '='.repeat(70));
console.log(`📊 测试汇总: ${pass} ✅ | ${fail} ❌ | 共 ${pass+fail}`);
console.log('='.repeat(70));
if (fail > 0) {
console.log('\n⚠️ 部分测试未通过,请检查以上 ❌ 项');
process.exit(1);
} else {
console.log('\n🎉 所有测试通过!用户管理功能闭环正常。');
}
await browser.close();
}
run().catch(e => { console.error('\n💥 测试异常:', e.message); process.exit(1); });