/** * 用户管理全生命周期测试 * * 覆盖场景: * - 三种角色(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); });