/** * Phase 2 + Phase 3 联合端到端测试 * * 测试内容: * 1. 认证中间件(IIT admin 路由需要 Token) * 2. RBAC 角色权限(不同角色看到不同内容) * 3. /my-projects API(用户-项目关联) * 4. 租户过滤(项目按 tenantId 隔离) * 5. 租户选项 API * 6. 项目创建带 tenantId * 7. IIT_OPERATOR 角色枚举 * 8. UserRole / TenantOption 类型完整性 * * 运行方式: npx tsx tests/e2e-phase2-phase3-test.ts */ const AUTH_BASE = 'http://localhost:3001/api/v1/auth'; const ADMIN_IIT_BASE = 'http://localhost:3001/api/v1/admin/iit-projects'; const IIT_BASE = 'http://localhost:3001/api/v1/iit'; let passCount = 0; let failCount = 0; const results: { name: string; ok: boolean; detail?: string }[] = []; // ========== Helpers ========== async function rawFetch(url: string, opts?: RequestInit) { const res = await fetch(url, opts); const json = await res.json().catch(() => null); return { status: res.status, data: json }; } async function authApi(method: string, url: string, token: string, body?: any) { const opts: RequestInit = { method, headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}`, }, }; if (body) opts.body = JSON.stringify(body); return rawFetch(url, opts); } async function login(phone: string, password: string = '123456'): Promise { const { status, data } = await rawFetch(`${AUTH_BASE}/login/password`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ phone, password }), }); if (status === 200 && data?.data?.tokens?.accessToken) { return data.data.tokens.accessToken; } return null; } function assert(name: string, condition: boolean, detail?: string) { if (condition) { passCount++; results.push({ name, ok: true }); console.log(` ✅ ${name}`); } else { failCount++; results.push({ name, ok: false, detail }); console.log(` ❌ ${name}${detail ? ` — ${detail}` : ''}`); } } // ========== Test Suites ========== async function testAuthMiddleware() { console.log('\n🔒 [1/8] IIT Admin 路由认证中间件'); // 无 Token 访问应返回 401 const { status: noTokenStatus } = await rawFetch(`${ADMIN_IIT_BASE}`, { method: 'GET' }); assert('无 Token 访问 GET /iit-projects → 401', noTokenStatus === 401, `status=${noTokenStatus}`); const { status: noTokenStatus2 } = await rawFetch(`${ADMIN_IIT_BASE}/tenant-options`, { method: 'GET' }); assert('无 Token 访问 GET /tenant-options → 401', noTokenStatus2 === 401, `status=${noTokenStatus2}`); } async function testRoleForbidden() { console.log('\n🚫 [2/8] RBAC: 普通 USER 不能访问 IIT Admin'); const userToken = await login('13800138003'); // 王医生, role=USER assert('普通用户登录成功', userToken !== null); if (userToken) { const { status } = await authApi('GET', ADMIN_IIT_BASE, userToken); assert('USER 角色访问 IIT Admin → 403', status === 403, `status=${status}`); } } async function testSuperAdminAccess() { console.log('\n👑 [3/8] SUPER_ADMIN 全权限访问'); const adminToken = await login('13800000001'); // 超级管理员 assert('SUPER_ADMIN 登录成功', adminToken !== null); if (adminToken) { const { status, data } = await authApi('GET', ADMIN_IIT_BASE, adminToken); assert('SUPER_ADMIN 访问项目列表 → 200', status === 200, `status=${status}`); assert('返回项目数组', Array.isArray(data?.data), `type=${typeof data?.data}`); // 项目带 tenantName const projects = data?.data || []; if (projects.length > 0) { assert('项目含 tenantName 字段', projects[0].tenantName !== undefined, `keys=${Object.keys(projects[0]).join(',')}`); assert('项目含 tenantId 字段', projects[0].tenantId !== undefined); } } return adminToken; } async function testTenantOptions(adminToken: string) { console.log('\n🏢 [4/8] 租户选项 API'); const { status, data } = await authApi('GET', `${ADMIN_IIT_BASE}/tenant-options`, adminToken); assert('GET /tenant-options → 200', status === 200, `status=${status}`); assert('返回租户数组', Array.isArray(data?.data), `type=${typeof data?.data}`); const tenants = data?.data || []; assert('租户数量 > 0', tenants.length > 0, `count=${tenants.length}`); if (tenants.length > 0) { assert('租户含 id/name/code/type', tenants[0].id && tenants[0].name && tenants[0].code && tenants[0].type, `keys=${Object.keys(tenants[0]).join(',')}`); } return tenants; } async function testTenantFiltering(adminToken: string) { console.log('\n🔍 [5/8] 租户过滤'); // 壹证循科技 的 tenantId const yizhengxunId = 'eb4e93b7-0210-4bf5-b853-cbea49cdadf8'; // 不带租户过滤 → 返回所有项目 const { data: allData } = await authApi('GET', ADMIN_IIT_BASE, adminToken); const allCount = allData?.data?.length || 0; assert('无过滤返回所有项目', allCount > 0, `count=${allCount}`); // 带租户过滤 → 只返回该租户项目 const { data: filteredData } = await authApi('GET', `${ADMIN_IIT_BASE}?tenantId=${yizhengxunId}`, adminToken); const filteredProjects = filteredData?.data || []; assert('按租户过滤返回结果', filteredProjects.length > 0, `count=${filteredProjects.length}`); assert('过滤结果中所有项目 tenantId 一致', filteredProjects.every((p: any) => p.tenantId === yizhengxunId), `tenantIds=${filteredProjects.map((p: any) => p.tenantId).join(',')}`); // 不存在的租户 → 返回空数组 const { data: emptyData } = await authApi('GET', `${ADMIN_IIT_BASE}?tenantId=non-existent-tenant`, adminToken); assert('不存在的租户返回空数组', (emptyData?.data?.length || 0) === 0); } async function testPharmaAdminIsolation() { console.log('\n🏭 [6/8] PHARMA_ADMIN 租户隔离'); const pharmaToken = await login('13800000006'); // 药企管理员, tenant=takeda assert('PHARMA_ADMIN 登录成功', pharmaToken !== null); if (pharmaToken) { const { status, data } = await authApi('GET', ADMIN_IIT_BASE, pharmaToken); assert('PHARMA_ADMIN 可以访问项目列表', status === 200, `status=${status}`); const projects = data?.data || []; // PHARMA_ADMIN 的 tenantId 是 tenant-takeda,而测试项目属于 壹证循科技 // 所以不应该看到测试项目 const takedaTenantId = 'tenant-takeda'; const allMatchTenant = projects.every((p: any) => p.tenantId === takedaTenantId); assert('PHARMA_ADMIN 只能看到自己租户的项目(或空)', projects.length === 0 || allMatchTenant, `count=${projects.length}, tenantIds=${projects.map((p: any) => p.tenantId).join(',')}`); } } async function testMyProjectsApi() { console.log('\n👤 [7/8] /my-projects API(用户-项目关联)'); // 无 Token → 401 const { status: noTokenStatus } = await rawFetch(`${IIT_BASE}/my-projects`, { method: 'GET' }); assert('无 Token 访问 /my-projects → 401', noTokenStatus === 401, `status=${noTokenStatus}`); // 登录用户(有 IitUserMapping 关联) const adminToken = await login('13800000001'); if (adminToken) { const { status, data } = await authApi('GET', `${IIT_BASE}/my-projects`, adminToken); assert('/my-projects 返回 200', status === 200, `status=${status}`); assert('返回 data 数组', Array.isArray(data?.data), `type=${typeof data?.data}`); // 当前超级管理员可能没有 IitUserMapping,所以可能是空数组 // 但 API 不应报错 const projects = data?.data || []; assert('/my-projects 不报错(即使无关联项目)', true, `count=${projects.length}`); if (projects.length > 0) { assert('项目含 myRole 字段', projects[0].myRole !== undefined, `keys=${Object.keys(projects[0]).join(',')}`); } } } async function testProjectDetailWithTenant(adminToken: string) { console.log('\n📋 [8/8] 项目详情含租户信息'); const projectId = 'test0102-pd-study'; const { status, data } = await authApi('GET', `${ADMIN_IIT_BASE}/${projectId}`, adminToken); assert('GET 项目详情 → 200', status === 200, `status=${status}`); const project = data?.data; assert('项目详情含 tenantId', project?.tenantId !== undefined && project?.tenantId !== null, `tenantId=${project?.tenantId}`); assert('项目详情含 tenant 对象', project?.tenant !== undefined, `tenant=${JSON.stringify(project?.tenant)}`); if (project?.tenant) { assert('tenant 含 name', !!project.tenant.name, `name=${project.tenant.name}`); } } // ========== Main ========== async function main() { console.log('🚀 Phase 2 + Phase 3 联合端到端测试'); console.log('='.repeat(60)); try { // Phase 2 Tests await testAuthMiddleware(); await testRoleForbidden(); const adminToken = await testSuperAdminAccess(); await testMyProjectsApi(); // Phase 3 Tests if (adminToken) { const tenants = await testTenantOptions(adminToken); await testTenantFiltering(adminToken); await testProjectDetailWithTenant(adminToken); } await testPharmaAdminIsolation(); } catch (err) { console.error('\n💥 测试执行异常:', err); failCount++; } // Summary console.log('\n' + '='.repeat(60)); console.log(`📊 测试结果: ${passCount} 通过, ${failCount} 失败, 共 ${passCount + failCount} 项`); if (failCount > 0) { console.log('\n❌ 失败项:'); results.filter(r => !r.ok).forEach(r => { console.log(` • ${r.name}${r.detail ? ` — ${r.detail}` : ''}`); }); } console.log('\n' + (failCount === 0 ? '🎉 全部通过!' : '⚠️ 有失败项,请检查')); process.exit(failCount > 0 ? 1 : 0); } main();