tyt-api-nest/test/e2e/specs/users.e2e-spec.ts
EL 6ec8891be5 修复 E2E 准备脚本:
package.json
test:e2e:prepare 现在是 migrate reset --force && prisma generate && seed
为 seed 运行时补充 JS Prisma client 生成器:
schema.prisma
修复 seed 在 ESM/CJS 下的 Prisma 导入兼容:
seed.mjs
修复 Jest 环境未加载 .env 导致连到 127.0.0.1 的问题:
e2e-app.helper.ts
修复夹具依赖“名称”导致被组织测试改名后失效的问题(改为按 seed openId 反查):
e2e-fixtures.helper.ts
修复组织测试的状态污染与清理逻辑,并收敛 afterAll 资源释放:
organization.e2e-spec.ts
e2e-context.helper.ts
2026-03-13 03:29:16 +08:00

333 lines
12 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 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('失败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({}),
});
});
});
});