import request from 'supertest'; import { Role } from '../../../src/generated/prisma/enums.js'; import { closeE2EContext, createE2EContext, type E2EContext, } from '../helpers/e2e-context.helper.js'; import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js'; import { expectErrorEnvelope, expectSuccessEnvelope, uniquePhone, uniqueSeedValue, } from '../helpers/e2e-http.helper.js'; describe('UsersController + BUsersController (e2e)', () => { let ctx: E2EContext; beforeAll(async () => { ctx = await createE2EContext(); }); afterAll(async () => { await closeE2EContext(ctx); }); async function createDoctorUser(token: string) { const response = await request(ctx.app.getHttpServer()) .post('/users') .set('Authorization', `Bearer ${token}`) .send({ name: uniqueSeedValue('用户-医生'), phone: uniquePhone(), password: 'Seed@1234', role: Role.DOCTOR, hospitalId: ctx.fixtures.hospitalAId, departmentId: ctx.fixtures.departmentA1Id, groupId: ctx.fixtures.groupA1Id, openId: uniqueSeedValue('users-doctor-openid'), }); expectSuccessEnvelope(response, 201); return response.body.data as { id: number; name: string }; } async function createEngineerUser(token: string) { const response = await request(ctx.app.getHttpServer()) .post('/users') .set('Authorization', `Bearer ${token}`) .send({ name: uniqueSeedValue('用户-工程师'), phone: uniquePhone(), password: 'Seed@1234', role: Role.ENGINEER, hospitalId: ctx.fixtures.hospitalAId, openId: uniqueSeedValue('users-engineer-openid'), }); expectSuccessEnvelope(response, 201); return response.body.data as { id: number; name: string }; } describe('POST /users', () => { it('成功:SYSTEM_ADMIN 可创建用户', async () => { await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]); }); it('失败:参数校验失败返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .post('/users') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ name: 'bad-user', phone: '123', password: 'short', role: Role.DOCTOR, hospitalId: ctx.fixtures.hospitalAId, departmentId: ctx.fixtures.departmentA1Id, groupId: ctx.fixtures.groupA1Id, }); expectErrorEnvelope(response, 400, 'phone 必须是合法手机号'); }); it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'POST /users role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 400, [Role.HOSPITAL_ADMIN]: 400, [Role.DIRECTOR]: 403, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, }, sendAsRole: async (_role, token) => request(ctx.app.getHttpServer()) .post('/users') .set('Authorization', `Bearer ${token}`) .send({}), sendWithoutToken: async () => request(ctx.app.getHttpServer()).post('/users').send({}), }); }); }); describe('GET /users', () => { it('成功:SYSTEM_ADMIN 可查询用户列表', async () => { const response = await request(ctx.app.getHttpServer()) .get('/users') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectSuccessEnvelope(response, 200); expect(Array.isArray(response.body.data)).toBe(true); }); it('失败:未登录返回 401', async () => { const response = await request(ctx.app.getHttpServer()).get('/users'); expectErrorEnvelope(response, 401, '缺少 Bearer Token'); }); it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'GET /users role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 200, [Role.HOSPITAL_ADMIN]: 200, [Role.DIRECTOR]: 403, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, }, sendAsRole: async (_role, token) => request(ctx.app.getHttpServer()) .get('/users') .set('Authorization', `Bearer ${token}`), sendWithoutToken: async () => request(ctx.app.getHttpServer()).get('/users'), }); }); }); describe('GET /users/:id', () => { it('成功:SYSTEM_ADMIN 可查询用户详情', async () => { const response = await request(ctx.app.getHttpServer()) .get(`/users/${ctx.fixtures.users.doctorAId}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectSuccessEnvelope(response, 200); expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId); }); it('失败:查询不存在用户返回 404', async () => { const response = await request(ctx.app.getHttpServer()) .get('/users/99999999') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectErrorEnvelope(response, 404, '用户不存在'); }); it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'GET /users/:id role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 200, [Role.HOSPITAL_ADMIN]: 200, [Role.DIRECTOR]: 403, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, }, sendAsRole: async (_role, token) => request(ctx.app.getHttpServer()) .get(`/users/${ctx.fixtures.users.doctorAId}`) .set('Authorization', `Bearer ${token}`), sendWithoutToken: async () => request(ctx.app.getHttpServer()).get( `/users/${ctx.fixtures.users.doctorAId}`, ), }); }); }); describe('PATCH /users/:id', () => { it('成功:SYSTEM_ADMIN 可更新用户姓名', async () => { const created = await createDoctorUser(ctx.tokens[Role.SYSTEM_ADMIN]); const nextName = uniqueSeedValue('更新后医生名'); const response = await request(ctx.app.getHttpServer()) .patch(`/users/${created.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ name: nextName }); expectSuccessEnvelope(response, 200); expect(response.body.data.name).toBe(nextName); }); it('失败:非医生调整科室/小组返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .patch(`/users/${ctx.fixtures.users.engineerAId}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ departmentId: ctx.fixtures.departmentA1Id, groupId: ctx.fixtures.groupA1Id, }); expectErrorEnvelope(response, 400, '仅医生/主任/组长允许调整科室/小组归属'); }); it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'PATCH /users/:id role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 404, [Role.HOSPITAL_ADMIN]: 404, [Role.DIRECTOR]: 403, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, }, sendAsRole: async (_role, token) => request(ctx.app.getHttpServer()) .patch('/users/99999999') .set('Authorization', `Bearer ${token}`) .send({ name: 'matrix-patch' }), sendWithoutToken: async () => request(ctx.app.getHttpServer()) .patch('/users/99999999') .send({ name: 'matrix-patch' }), }); }); }); describe('DELETE /users/:id', () => { it('成功:SYSTEM_ADMIN 可删除用户', async () => { const created = await createEngineerUser(ctx.tokens[Role.SYSTEM_ADMIN]); const response = await request(ctx.app.getHttpServer()) .delete(`/users/${created.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectSuccessEnvelope(response, 200); expect(response.body.data.id).toBe(created.id); }); it('失败:存在关联患者/任务时返回 409', async () => { const response = await request(ctx.app.getHttpServer()) .delete(`/users/${ctx.fixtures.users.doctorAId}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectErrorEnvelope(response, 409, '用户存在关联患者或任务,无法删除'); }); it('失败:HOSPITAL_ADMIN 无法删除返回 403', async () => { const response = await request(ctx.app.getHttpServer()) .delete(`/users/${ctx.fixtures.users.doctorAId}`) .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); expectErrorEnvelope(response, 403, '无权限执行当前操作'); }); it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'DELETE /users/:id role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 404, [Role.HOSPITAL_ADMIN]: 403, [Role.DIRECTOR]: 403, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, }, sendAsRole: async (_role, token) => request(ctx.app.getHttpServer()) .delete('/users/99999999') .set('Authorization', `Bearer ${token}`), sendWithoutToken: async () => request(ctx.app.getHttpServer()).delete('/users/99999999'), }); }); }); describe('PATCH /b/users/:id/assign-engineer-hospital', () => { it('成功:SYSTEM_ADMIN 可绑定工程师医院', async () => { const response = await request(ctx.app.getHttpServer()) .patch( `/b/users/${ctx.fixtures.users.engineerAId}/assign-engineer-hospital`, ) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ hospitalId: ctx.fixtures.hospitalAId }); expectSuccessEnvelope(response, 200); expect(response.body.data.hospitalId).toBe(ctx.fixtures.hospitalAId); expect(response.body.data.role).toBe(Role.ENGINEER); }); it('失败:目标用户不是工程师返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .patch( `/b/users/${ctx.fixtures.users.doctorAId}/assign-engineer-hospital`, ) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ hospitalId: ctx.fixtures.hospitalAId }); expectErrorEnvelope(response, 400, '目标用户不是工程师'); }); it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'PATCH /b/users/:id/assign-engineer-hospital role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 400, [Role.HOSPITAL_ADMIN]: 403, [Role.DIRECTOR]: 403, [Role.LEADER]: 403, [Role.DOCTOR]: 403, [Role.ENGINEER]: 403, }, sendAsRole: async (_role, token) => request(ctx.app.getHttpServer()) .patch( `/b/users/${ctx.fixtures.users.engineerAId}/assign-engineer-hospital`, ) .set('Authorization', `Bearer ${token}`) .send({}), sendWithoutToken: async () => request(ctx.app.getHttpServer()) .patch( `/b/users/${ctx.fixtures.users.engineerAId}/assign-engineer-hospital`, ) .send({}), }); }); }); });