tyt-api-nest/test/e2e/specs/users.e2e-spec.ts
EL 2bfe8ac8c8 新增上传资产模型与迁移,支持 IMAGE、VIDEO、FILE 三类资产管理
新增 B 端上传接口与列表接口,统一文件上传和分页查询能力
上传能力支持医院级数据隔离:系统管理员需显式指定医院,院内角色按登录医院自动隔离
图片上传自动压缩并转为 webp,视频上传自动转码并压缩为 mp4,普通文件按原始类型存储
增加上传目录与公开访问能力,统一输出可直接预览的访问地址
前端新增影像库页面,支持按类型筛选、关键字检索、分页浏览、在线预览与原文件访问
前端新增通用上传组件,支持在页面内复用并返回上传结果
管理后台新增影像库菜单与路由,并补充页面级角色权限控制
患者手术相关表单接入上传复用能力,支持术前资料与设备标签上传回填
新增上传模块 e2e 用例,覆盖成功路径、权限矩阵与关键失败场景
补充上传模块文档与安装依赖说明,完善工程内使用说明
2026-03-20 04:35:43 +08:00

524 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 createLeaderUser(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.LEADER,
hospitalId: ctx.fixtures.hospitalAId,
departmentId: ctx.fixtures.departmentA1Id,
groupId: ctx.fixtures.groupA1Id,
openId: uniqueSeedValue('users-leader-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('成功HOSPITAL_ADMIN 可创建本院医生', async () => {
await createDoctorUser(ctx.tokens[Role.HOSPITAL_ADMIN]);
});
it('成功HOSPITAL_ADMIN 可创建本院组长', async () => {
await createLeaderUser(ctx.tokens[Role.HOSPITAL_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('失败DIRECTOR 创建用户返回 403', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`)
.send({
name: uniqueSeedValue('主任创建用户'),
phone: uniquePhone(),
password: 'Seed@1234',
role: Role.DOCTOR,
hospitalId: ctx.fixtures.hospitalAId,
departmentId: ctx.fixtures.departmentA1Id,
groupId: ctx.fixtures.groupA1Id,
});
expectErrorEnvelope(response, 403, '无权限执行当前操作');
});
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('成功DIRECTOR 可查看本科室下级列表', async () => {
const response = await request(ctx.app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
expectSuccessEnvelope(response, 200);
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.data.length).toBeGreaterThan(0);
response.body.data.forEach((item: { role: Role; departmentId: number }) => {
expect([Role.DOCTOR, Role.LEADER]).toContain(item.role);
expect(item.departmentId).toBe(ctx.fixtures.departmentA1Id);
});
});
it('成功LEADER 可查看本组医生列表', async () => {
const response = await request(ctx.app.getHttpServer())
.get('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.LEADER]}`);
expectSuccessEnvelope(response, 200);
expect(Array.isArray(response.body.data)).toBe(true);
expect(response.body.data.length).toBeGreaterThan(0);
response.body.data.forEach((item: { role: Role; groupId: number }) => {
expect(item.role).toBe(Role.DOCTOR);
expect(item.groupId).toBe(ctx.fixtures.groupA1Id);
});
});
it('失败:未登录返回 401', async () => {
const response = await request(ctx.app.getHttpServer()).get('/users');
expectErrorEnvelope(response, 401, '缺少 Bearer Token');
});
it('角色矩阵SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR/LEADER 可访问,其余角色 403未登录 401', async () => {
await assertRoleMatrix({
name: 'GET /users role matrix',
tokens: ctx.tokens,
expectedStatusByRole: {
[Role.SYSTEM_ADMIN]: 200,
[Role.HOSPITAL_ADMIN]: 200,
[Role.DIRECTOR]: 200,
[Role.LEADER]: 200,
[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('成功DIRECTOR 可查询本科室医生详情', async () => {
const response = await request(ctx.app.getHttpServer())
.get(`/users/${ctx.fixtures.users.doctorAId}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
expectSuccessEnvelope(response, 200);
expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId);
expect(response.body.data.departmentId).toBe(ctx.fixtures.departmentA1Id);
});
it('成功LEADER 可查询本组医生详情', async () => {
const response = await request(ctx.app.getHttpServer())
.get(`/users/${ctx.fixtures.users.doctorAId}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.LEADER]}`);
expectSuccessEnvelope(response, 200);
expect(response.body.data.id).toBe(ctx.fixtures.users.doctorAId);
expect(response.body.data.groupId).toBe(ctx.fixtures.groupA1Id);
});
it('失败DIRECTOR 查询跨科室医生返回 403', async () => {
const response = await request(ctx.app.getHttpServer())
.get(`/users/${ctx.fixtures.users.doctorA3Id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
expectErrorEnvelope(response, 403, '无权限执行当前操作');
});
it('失败LEADER 查询其他小组医生返回 403', async () => {
const response = await request(ctx.app.getHttpServer())
.get(`/users/${ctx.fixtures.users.doctorA3Id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.LEADER]}`);
expectErrorEnvelope(response, 403, '无权限执行当前操作');
});
it('角色矩阵SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR/LEADER 可访问,其他角色 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]: 200,
[Role.LEADER]: 200,
[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('成功HOSPITAL_ADMIN 可更新本院医生姓名', async () => {
const created = await createDoctorUser(ctx.tokens[Role.HOSPITAL_ADMIN]);
const nextName = uniqueSeedValue('主任更新医生名');
const response = await request(ctx.app.getHttpServer())
.patch(`/users/${created.id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
.send({ name: nextName });
expectSuccessEnvelope(response, 200);
expect(response.body.data.name).toBe(nextName);
});
it('成功HOSPITAL_ADMIN 可将本院医生调整为组长', async () => {
const created = await createDoctorUser(ctx.tokens[Role.HOSPITAL_ADMIN]);
const response = await request(ctx.app.getHttpServer())
.patch(`/users/${created.id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
.send({ role: Role.LEADER, groupId: ctx.fixtures.groupA1Id });
expectSuccessEnvelope(response, 200);
expect(response.body.data.role).toBe(Role.LEADER);
});
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('失败DIRECTOR 更新用户返回 403', async () => {
const response = await request(ctx.app.getHttpServer())
.patch(`/users/${ctx.fixtures.users.doctorAId}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`)
.send({ name: uniqueSeedValue('主任越权更新') });
expectErrorEnvelope(response, 403, '无权限执行当前操作');
});
it('失败HOSPITAL_ADMIN 不能把用户改成医院管理员', async () => {
const response = await request(ctx.app.getHttpServer())
.patch(`/users/${ctx.fixtures.users.doctorAId}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
.send({ role: Role.HOSPITAL_ADMIN });
expectErrorEnvelope(response, 403, '医院管理员仅可操作本院非管理员账号');
});
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('成功HOSPITAL_ADMIN 可删除本院医生', async () => {
const created = await createDoctorUser(ctx.tokens[Role.HOSPITAL_ADMIN]);
const response = await request(ctx.app.getHttpServer())
.delete(`/users/${created.id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
expectSuccessEnvelope(response, 200);
expect(response.body.data.id).toBe(created.id);
});
it('成功HOSPITAL_ADMIN 可删除本院组长', async () => {
const created = await createLeaderUser(ctx.tokens[Role.HOSPITAL_ADMIN]);
const response = await request(ctx.app.getHttpServer())
.delete(`/users/${created.id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_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('失败DIRECTOR 删除用户返回 403', async () => {
const response = await request(ctx.app.getHttpServer())
.delete(`/users/${ctx.fixtures.users.doctorA3Id}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
expectErrorEnvelope(response, 403, '无权限执行当前操作');
});
it('失败HOSPITAL_ADMIN 无法删除本院管理员', async () => {
const response = await request(ctx.app.getHttpServer())
.delete(`/users/${ctx.fixtures.users.hospitalAdminAId}`)
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
expectErrorEnvelope(response, 403, '医院管理员仅可操作本院非管理员账号');
});
it('角色矩阵SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403未登录 401', async () => {
await assertRoleMatrix({
name: 'DELETE /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())
.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({}),
});
});
});
});