修复 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
This commit is contained in:
parent
b55e600c9c
commit
6ec8891be5
60
docs/e2e-testing.md
Normal file
60
docs/e2e-testing.md
Normal file
@ -0,0 +1,60 @@
|
||||
# E2E 接口测试说明
|
||||
|
||||
## 1. 目标
|
||||
|
||||
- 覆盖 `src/**/*controller.ts` 当前全部 30 个业务接口。
|
||||
- 采用 `supertest + @nestjs/testing` 进行真实 HTTP E2E 测试。
|
||||
- 测试前固定执行数据库重置与 seed,确保结果可重复。
|
||||
|
||||
## 2. 风险提示
|
||||
|
||||
`pnpm test:e2e` 会执行:
|
||||
|
||||
1. `prisma migrate reset --force`
|
||||
2. `node prisma/seed.mjs`
|
||||
|
||||
这会清空 `.env` 中 `DATABASE_URL` 指向数据库的全部数据,请仅在测试库执行。
|
||||
|
||||
## 3. 运行命令
|
||||
|
||||
```bash
|
||||
pnpm test:e2e
|
||||
```
|
||||
|
||||
仅重置数据库并注入 seed:
|
||||
|
||||
```bash
|
||||
pnpm test:e2e:prepare
|
||||
```
|
||||
|
||||
监听模式:
|
||||
|
||||
```bash
|
||||
pnpm test:e2e:watch
|
||||
```
|
||||
|
||||
## 4. 种子账号(默认密码:`Seed@1234`)
|
||||
|
||||
- 系统管理员:`13800001000`
|
||||
- 院管(医院 A):`13800001001`
|
||||
- 主任(医院 A):`13800001002`
|
||||
- 组长(医院 A):`13800001003`
|
||||
- 医生(医院 A):`13800001004`
|
||||
- 工程师(医院 A):`13800001005`
|
||||
|
||||
## 5. 用例结构
|
||||
|
||||
- `test/e2e/specs/auth.e2e-spec.ts`
|
||||
- `test/e2e/specs/users.e2e-spec.ts`
|
||||
- `test/e2e/specs/organization.e2e-spec.ts`
|
||||
- `test/e2e/specs/tasks.e2e-spec.ts`
|
||||
- `test/e2e/specs/patients.e2e-spec.ts`
|
||||
|
||||
## 6. 覆盖策略
|
||||
|
||||
- 受保护接口(27 个):每个接口覆盖 6 角色访问结果 + 未登录 401。
|
||||
- 非受保护接口(3 个):每个接口至少 1 个成功 + 1 个失败。
|
||||
- 关键行为额外覆盖:
|
||||
- 任务状态机冲突(409)
|
||||
- 患者 B 端角色可见性
|
||||
- 组织域院管作用域限制与删除冲突
|
||||
@ -12,7 +12,10 @@
|
||||
"start": "nest start",
|
||||
"start:dev": "nest start --watch",
|
||||
"start:debug": "nest start --debug --watch",
|
||||
"start:prod": "node dist/main"
|
||||
"start:prod": "node dist/main",
|
||||
"test:e2e:prepare": "pnpm prisma migrate reset --force && pnpm prisma generate && node prisma/seed.mjs",
|
||||
"test:e2e": "pnpm test:e2e:prepare && NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ./test/jest-e2e.config.cjs --runInBand",
|
||||
"test:e2e:watch": "NODE_OPTIONS=--experimental-vm-modules pnpm exec jest --config ./test/jest-e2e.config.cjs --watch"
|
||||
},
|
||||
"dependencies": {
|
||||
"@nestjs/common": "^11.0.1",
|
||||
@ -39,14 +42,17 @@
|
||||
"@nestjs/testing": "^11.0.1",
|
||||
"@types/bcrypt": "^6.0.0",
|
||||
"@types/express": "^5.0.0",
|
||||
"@types/jest": "^30.0.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^22.10.7",
|
||||
"@types/supertest": "^6.0.2",
|
||||
"globals": "^16.0.0",
|
||||
"jest": "^30.3.0",
|
||||
"prettier": "^3.4.2",
|
||||
"prisma": "^7.4.2",
|
||||
"source-map-support": "^0.5.21",
|
||||
"supertest": "^7.0.0",
|
||||
"ts-jest": "^29.4.6",
|
||||
"ts-loader": "^9.5.2",
|
||||
"ts-node": "^10.9.2",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
|
||||
2374
pnpm-lock.yaml
generated
2374
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@ -3,6 +3,11 @@ generator client {
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
// 兼容 seed 脚本在 Node.js 直接运行时使用 @prisma/client runtime。
|
||||
generator seed_client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
519
prisma/seed.mjs
519
prisma/seed.mjs
@ -1,10 +1,10 @@
|
||||
import 'dotenv/config';
|
||||
import { PrismaPg } from '@prisma/adapter-pg';
|
||||
import { hash } from 'bcrypt';
|
||||
import { PrismaClient } from '../src/generated/prisma/client.js';
|
||||
import { DeviceStatus, Role } from '../src/generated/prisma/enums.js';
|
||||
import prismaClientPackage from '@prisma/client';
|
||||
|
||||
const { DeviceStatus, PrismaClient, Role, TaskStatus } = prismaClientPackage;
|
||||
|
||||
// Keep the seed executable with the same pg driver adapter used by PrismaService.
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
throw new Error('DATABASE_URL is required to run seed');
|
||||
@ -14,177 +14,424 @@ const prisma = new PrismaClient({
|
||||
adapter: new PrismaPg({ connectionString }),
|
||||
});
|
||||
|
||||
async function main() {
|
||||
// Default seed login password (plain): Seed@1234
|
||||
const seedPasswordHash = await hash('Seed@1234', 12);
|
||||
const SEED_PASSWORD_PLAIN = 'Seed@1234';
|
||||
|
||||
// Seed a baseline organization tree for local/demo usage.
|
||||
const hospital =
|
||||
(await prisma.hospital.findFirst({ where: { name: 'Demo Hospital' } })) ??
|
||||
(await prisma.hospital.create({
|
||||
data: { name: 'Demo Hospital' },
|
||||
}));
|
||||
async function ensureHospital(name) {
|
||||
return (
|
||||
(await prisma.hospital.findFirst({ where: { name } })) ??
|
||||
prisma.hospital.create({ data: { name } })
|
||||
);
|
||||
}
|
||||
|
||||
const department =
|
||||
async function ensureDepartment(hospitalId, name) {
|
||||
return (
|
||||
(await prisma.department.findFirst({
|
||||
where: {
|
||||
hospitalId: hospital.id,
|
||||
name: 'Neurosurgery',
|
||||
},
|
||||
where: { hospitalId, name },
|
||||
})) ??
|
||||
(await prisma.department.create({
|
||||
data: {
|
||||
hospitalId: hospital.id,
|
||||
name: 'Neurosurgery',
|
||||
},
|
||||
}));
|
||||
prisma.department.create({
|
||||
data: { hospitalId, name },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
const group =
|
||||
async function ensureGroup(departmentId, name) {
|
||||
return (
|
||||
(await prisma.group.findFirst({
|
||||
where: {
|
||||
departmentId: department.id,
|
||||
name: 'Shift-A',
|
||||
},
|
||||
where: { departmentId, name },
|
||||
})) ??
|
||||
(await prisma.group.create({
|
||||
data: {
|
||||
departmentId: department.id,
|
||||
name: 'Shift-A',
|
||||
},
|
||||
}));
|
||||
prisma.group.create({
|
||||
data: { departmentId, name },
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
// Use openId as idempotent unique key for seeded users.
|
||||
const systemAdmin = await prisma.user.upsert({
|
||||
where: { openId: 'seed-system-admin-openid' },
|
||||
update: {
|
||||
name: 'System Admin',
|
||||
phone: '13800000000',
|
||||
async function upsertUserByOpenId(openId, data) {
|
||||
return prisma.user.upsert({
|
||||
where: { openId },
|
||||
update: data,
|
||||
create: {
|
||||
...data,
|
||||
openId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function ensurePatient({
|
||||
hospitalId,
|
||||
doctorId,
|
||||
name,
|
||||
phone,
|
||||
idCardHash,
|
||||
}) {
|
||||
const existing = await prisma.patient.findFirst({
|
||||
where: {
|
||||
hospitalId,
|
||||
phone,
|
||||
idCardHash,
|
||||
},
|
||||
});
|
||||
|
||||
if (existing) {
|
||||
if (existing.doctorId !== doctorId || existing.name !== name) {
|
||||
return prisma.patient.update({
|
||||
where: { id: existing.id },
|
||||
data: { doctorId, name },
|
||||
});
|
||||
}
|
||||
return existing;
|
||||
}
|
||||
|
||||
return prisma.patient.create({
|
||||
data: {
|
||||
hospitalId,
|
||||
doctorId,
|
||||
name,
|
||||
phone,
|
||||
idCardHash,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const seedPasswordHash = await hash(SEED_PASSWORD_PLAIN, 12);
|
||||
|
||||
const hospitalA = await ensureHospital('Seed Hospital A');
|
||||
const hospitalB = await ensureHospital('Seed Hospital B');
|
||||
|
||||
const departmentA1 = await ensureDepartment(hospitalA.id, 'Neurosurgery-A1');
|
||||
const departmentA2 = await ensureDepartment(hospitalA.id, 'Cardiology-A2');
|
||||
const departmentB1 = await ensureDepartment(hospitalB.id, 'Neurosurgery-B1');
|
||||
|
||||
const groupA1 = await ensureGroup(departmentA1.id, 'Shift-A1');
|
||||
const groupA2 = await ensureGroup(departmentA2.id, 'Shift-A2');
|
||||
const groupB1 = await ensureGroup(departmentB1.id, 'Shift-B1');
|
||||
|
||||
const systemAdmin = await upsertUserByOpenId('seed-system-admin-openid', {
|
||||
name: 'Seed System Admin',
|
||||
phone: '13800001000',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.SYSTEM_ADMIN,
|
||||
hospitalId: null,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
},
|
||||
create: {
|
||||
name: 'System Admin',
|
||||
phone: '13800000000',
|
||||
passwordHash: seedPasswordHash,
|
||||
openId: 'seed-system-admin-openid',
|
||||
role: Role.SYSTEM_ADMIN,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { openId: 'seed-hospital-admin-openid' },
|
||||
update: {
|
||||
name: 'Hospital Admin',
|
||||
phone: '13800000001',
|
||||
const hospitalAdminA = await upsertUserByOpenId(
|
||||
'seed-hospital-admin-a-openid',
|
||||
{
|
||||
name: 'Seed Hospital Admin A',
|
||||
phone: '13800001001',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.HOSPITAL_ADMIN,
|
||||
hospitalId: hospital.id,
|
||||
departmentId: department.id,
|
||||
groupId: group.id,
|
||||
},
|
||||
create: {
|
||||
name: 'Hospital Admin',
|
||||
phone: '13800000001',
|
||||
passwordHash: seedPasswordHash,
|
||||
openId: 'seed-hospital-admin-openid',
|
||||
role: Role.HOSPITAL_ADMIN,
|
||||
hospitalId: hospital.id,
|
||||
departmentId: department.id,
|
||||
groupId: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
const doctor = await prisma.user.upsert({
|
||||
where: { openId: 'seed-doctor-openid' },
|
||||
update: {
|
||||
name: 'Doctor Demo',
|
||||
phone: '13800000002',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: hospital.id,
|
||||
departmentId: department.id,
|
||||
groupId: group.id,
|
||||
},
|
||||
create: {
|
||||
name: 'Doctor Demo',
|
||||
phone: '13800000002',
|
||||
passwordHash: seedPasswordHash,
|
||||
openId: 'seed-doctor-openid',
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: hospital.id,
|
||||
departmentId: department.id,
|
||||
groupId: group.id,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.user.upsert({
|
||||
where: { openId: 'seed-engineer-openid' },
|
||||
update: {
|
||||
name: 'Engineer Demo',
|
||||
phone: '13800000009',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.ENGINEER,
|
||||
hospitalId: hospital.id,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
},
|
||||
create: {
|
||||
name: 'Engineer Demo',
|
||||
phone: '13800000009',
|
||||
);
|
||||
|
||||
await upsertUserByOpenId('seed-hospital-admin-b-openid', {
|
||||
name: 'Seed Hospital Admin B',
|
||||
phone: '13800001101',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.HOSPITAL_ADMIN,
|
||||
hospitalId: hospitalB.id,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
});
|
||||
|
||||
const directorA = await upsertUserByOpenId('seed-director-a-openid', {
|
||||
name: 'Seed Director A',
|
||||
phone: '13800001002',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.DIRECTOR,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: departmentA1.id,
|
||||
groupId: null,
|
||||
});
|
||||
|
||||
const leaderA = await upsertUserByOpenId('seed-leader-a-openid', {
|
||||
name: 'Seed Leader A',
|
||||
phone: '13800001003',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.LEADER,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: departmentA1.id,
|
||||
groupId: groupA1.id,
|
||||
});
|
||||
|
||||
const doctorA = await upsertUserByOpenId('seed-doctor-a-openid', {
|
||||
name: 'Seed Doctor A',
|
||||
phone: '13800001004',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: departmentA1.id,
|
||||
groupId: groupA1.id,
|
||||
});
|
||||
|
||||
const doctorA2 = await upsertUserByOpenId('seed-doctor-a2-openid', {
|
||||
name: 'Seed Doctor A2',
|
||||
phone: '13800001204',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: departmentA1.id,
|
||||
groupId: groupA1.id,
|
||||
});
|
||||
|
||||
const doctorA3 = await upsertUserByOpenId('seed-doctor-a3-openid', {
|
||||
name: 'Seed Doctor A3',
|
||||
phone: '13800001304',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: departmentA2.id,
|
||||
groupId: groupA2.id,
|
||||
});
|
||||
|
||||
const doctorB = await upsertUserByOpenId('seed-doctor-b-openid', {
|
||||
name: 'Seed Doctor B',
|
||||
phone: '13800001104',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: hospitalB.id,
|
||||
departmentId: departmentB1.id,
|
||||
groupId: groupB1.id,
|
||||
});
|
||||
|
||||
const engineerA = await upsertUserByOpenId('seed-engineer-a-openid', {
|
||||
name: 'Seed Engineer A',
|
||||
phone: '13800001005',
|
||||
passwordHash: seedPasswordHash,
|
||||
openId: 'seed-engineer-openid',
|
||||
role: Role.ENGINEER,
|
||||
hospitalId: hospital.id,
|
||||
hospitalId: hospitalA.id,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
});
|
||||
|
||||
const engineerB = await upsertUserByOpenId('seed-engineer-b-openid', {
|
||||
name: 'Seed Engineer B',
|
||||
phone: '13800001105',
|
||||
passwordHash: seedPasswordHash,
|
||||
role: Role.ENGINEER,
|
||||
hospitalId: hospitalB.id,
|
||||
departmentId: null,
|
||||
groupId: null,
|
||||
});
|
||||
|
||||
const patientA1 = await ensurePatient({
|
||||
hospitalId: hospitalA.id,
|
||||
doctorId: doctorA.id,
|
||||
name: 'Seed Patient A1',
|
||||
phone: '13800002001',
|
||||
idCardHash: 'seed-id-card-cross-hospital',
|
||||
});
|
||||
|
||||
const patientA2 = await ensurePatient({
|
||||
hospitalId: hospitalA.id,
|
||||
doctorId: doctorA2.id,
|
||||
name: 'Seed Patient A2',
|
||||
phone: '13800002002',
|
||||
idCardHash: 'seed-id-card-a2',
|
||||
});
|
||||
|
||||
const patientA3 = await ensurePatient({
|
||||
hospitalId: hospitalA.id,
|
||||
doctorId: doctorA3.id,
|
||||
name: 'Seed Patient A3',
|
||||
phone: '13800002003',
|
||||
idCardHash: 'seed-id-card-a3',
|
||||
});
|
||||
|
||||
const patientB1 = await ensurePatient({
|
||||
hospitalId: hospitalB.id,
|
||||
doctorId: doctorB.id,
|
||||
name: 'Seed Patient B1',
|
||||
phone: '13800002001',
|
||||
idCardHash: 'seed-id-card-cross-hospital',
|
||||
});
|
||||
|
||||
const deviceA1 = await prisma.device.upsert({
|
||||
where: { snCode: 'SEED-SN-A-001' },
|
||||
update: {
|
||||
patientId: patientA1.id,
|
||||
currentPressure: 118,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
create: {
|
||||
snCode: 'SEED-SN-A-001',
|
||||
patientId: patientA1.id,
|
||||
currentPressure: 118,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
const patient =
|
||||
(await prisma.patient.findFirst({
|
||||
where: {
|
||||
hospitalId: hospital.id,
|
||||
phone: '13800000003',
|
||||
idCardHash: 'seed-id-card-hash',
|
||||
},
|
||||
})) ??
|
||||
(await prisma.patient.create({
|
||||
data: {
|
||||
hospitalId: hospital.id,
|
||||
doctorId: doctor.id,
|
||||
name: 'Patient Demo',
|
||||
phone: '13800000003',
|
||||
idCardHash: 'seed-id-card-hash',
|
||||
},
|
||||
}));
|
||||
|
||||
await prisma.device.upsert({
|
||||
where: { snCode: 'SEED-SN-001' },
|
||||
const deviceA2 = await prisma.device.upsert({
|
||||
where: { snCode: 'SEED-SN-A-002' },
|
||||
update: {
|
||||
patientId: patient.id,
|
||||
currentPressure: 110,
|
||||
patientId: patientA2.id,
|
||||
currentPressure: 112,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
create: {
|
||||
snCode: 'SEED-SN-001',
|
||||
patientId: patient.id,
|
||||
currentPressure: 110,
|
||||
snCode: 'SEED-SN-A-002',
|
||||
patientId: patientA2.id,
|
||||
currentPressure: 112,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.device.upsert({
|
||||
where: { snCode: 'SEED-SN-A-003' },
|
||||
update: {
|
||||
patientId: patientA3.id,
|
||||
currentPressure: 109,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
create: {
|
||||
snCode: 'SEED-SN-A-003',
|
||||
patientId: patientA3.id,
|
||||
currentPressure: 109,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
const deviceB1 = await prisma.device.upsert({
|
||||
where: { snCode: 'SEED-SN-B-001' },
|
||||
update: {
|
||||
patientId: patientB1.id,
|
||||
currentPressure: 121,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
create: {
|
||||
snCode: 'SEED-SN-B-001',
|
||||
patientId: patientB1.id,
|
||||
currentPressure: 121,
|
||||
status: DeviceStatus.ACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
await prisma.device.upsert({
|
||||
where: { snCode: 'SEED-SN-A-004' },
|
||||
update: {
|
||||
patientId: patientA1.id,
|
||||
currentPressure: 130,
|
||||
status: DeviceStatus.INACTIVE,
|
||||
},
|
||||
create: {
|
||||
snCode: 'SEED-SN-A-004',
|
||||
patientId: patientA1.id,
|
||||
currentPressure: 130,
|
||||
status: DeviceStatus.INACTIVE,
|
||||
},
|
||||
});
|
||||
|
||||
// 清理与种子设备关联的历史任务,保证 seed 可重复执行且生命周期夹具稳定。
|
||||
const seedTaskItems = await prisma.taskItem.findMany({
|
||||
where: {
|
||||
deviceId: {
|
||||
in: [deviceA1.id, deviceB1.id],
|
||||
},
|
||||
},
|
||||
select: { taskId: true },
|
||||
});
|
||||
const seedTaskIds = Array.from(
|
||||
new Set(seedTaskItems.map((item) => item.taskId)),
|
||||
);
|
||||
if (seedTaskIds.length > 0) {
|
||||
await prisma.task.deleteMany({
|
||||
where: {
|
||||
id: {
|
||||
in: seedTaskIds,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const lifecycleTaskA = await prisma.task.create({
|
||||
data: {
|
||||
status: TaskStatus.COMPLETED,
|
||||
creatorId: doctorA.id,
|
||||
engineerId: engineerA.id,
|
||||
hospitalId: hospitalA.id,
|
||||
items: {
|
||||
create: [
|
||||
{
|
||||
deviceId: deviceA1.id,
|
||||
oldPressure: 118,
|
||||
targetPressure: 120,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: { items: true },
|
||||
});
|
||||
|
||||
const lifecycleTaskB = await prisma.task.create({
|
||||
data: {
|
||||
status: TaskStatus.PENDING,
|
||||
creatorId: doctorB.id,
|
||||
engineerId: engineerB.id,
|
||||
hospitalId: hospitalB.id,
|
||||
items: {
|
||||
create: [
|
||||
{
|
||||
deviceId: deviceB1.id,
|
||||
oldPressure: 121,
|
||||
targetPressure: 119,
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
include: { items: true },
|
||||
});
|
||||
|
||||
console.log(
|
||||
JSON.stringify(
|
||||
{
|
||||
ok: true,
|
||||
hospitalId: hospital.id,
|
||||
departmentId: department.id,
|
||||
groupId: group.id,
|
||||
seedPasswordPlain: SEED_PASSWORD_PLAIN,
|
||||
hospitals: {
|
||||
hospitalAId: hospitalA.id,
|
||||
hospitalBId: hospitalB.id,
|
||||
},
|
||||
departments: {
|
||||
departmentA1Id: departmentA1.id,
|
||||
departmentA2Id: departmentA2.id,
|
||||
departmentB1Id: departmentB1.id,
|
||||
},
|
||||
groups: {
|
||||
groupA1Id: groupA1.id,
|
||||
groupA2Id: groupA2.id,
|
||||
groupB1Id: groupB1.id,
|
||||
},
|
||||
users: {
|
||||
systemAdminId: systemAdmin.id,
|
||||
doctorId: doctor.id,
|
||||
patientId: patient.id,
|
||||
seedPasswordPlain: 'Seed@1234',
|
||||
hospitalAdminAId: hospitalAdminA.id,
|
||||
directorAId: directorA.id,
|
||||
leaderAId: leaderA.id,
|
||||
doctorAId: doctorA.id,
|
||||
doctorA2Id: doctorA2.id,
|
||||
doctorA3Id: doctorA3.id,
|
||||
doctorBId: doctorB.id,
|
||||
engineerAId: engineerA.id,
|
||||
engineerBId: engineerB.id,
|
||||
},
|
||||
patients: {
|
||||
patientA1Id: patientA1.id,
|
||||
patientA2Id: patientA2.id,
|
||||
patientA3Id: patientA3.id,
|
||||
patientB1Id: patientB1.id,
|
||||
},
|
||||
devices: {
|
||||
deviceA1Id: deviceA1.id,
|
||||
deviceA2Id: deviceA2.id,
|
||||
deviceB1Id: deviceB1.id,
|
||||
},
|
||||
tasks: {
|
||||
lifecycleTaskAId: lifecycleTaskA.id,
|
||||
lifecycleTaskBId: lifecycleTaskB.id,
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
|
||||
59
test/e2e/fixtures/e2e-roles.ts
Normal file
59
test/e2e/fixtures/e2e-roles.ts
Normal file
@ -0,0 +1,59 @@
|
||||
import { Role } from '../../../src/generated/prisma/enums.js';
|
||||
|
||||
export const E2E_SEED_PASSWORD = 'Seed@1234';
|
||||
|
||||
export const E2E_ROLE_LIST = [
|
||||
Role.SYSTEM_ADMIN,
|
||||
Role.HOSPITAL_ADMIN,
|
||||
Role.DIRECTOR,
|
||||
Role.LEADER,
|
||||
Role.DOCTOR,
|
||||
Role.ENGINEER,
|
||||
] as const;
|
||||
|
||||
export type E2ERole = (typeof E2E_ROLE_LIST)[number];
|
||||
|
||||
export interface E2ESeedCredential {
|
||||
role: E2ERole;
|
||||
phone: string;
|
||||
password: string;
|
||||
hospitalId?: number;
|
||||
}
|
||||
|
||||
export const E2E_SEED_CREDENTIALS: Record<E2ERole, E2ESeedCredential> = {
|
||||
[Role.SYSTEM_ADMIN]: {
|
||||
role: Role.SYSTEM_ADMIN,
|
||||
phone: '13800001000',
|
||||
password: E2E_SEED_PASSWORD,
|
||||
},
|
||||
[Role.HOSPITAL_ADMIN]: {
|
||||
role: Role.HOSPITAL_ADMIN,
|
||||
phone: '13800001001',
|
||||
password: E2E_SEED_PASSWORD,
|
||||
hospitalId: 1,
|
||||
},
|
||||
[Role.DIRECTOR]: {
|
||||
role: Role.DIRECTOR,
|
||||
phone: '13800001002',
|
||||
password: E2E_SEED_PASSWORD,
|
||||
hospitalId: 1,
|
||||
},
|
||||
[Role.LEADER]: {
|
||||
role: Role.LEADER,
|
||||
phone: '13800001003',
|
||||
password: E2E_SEED_PASSWORD,
|
||||
hospitalId: 1,
|
||||
},
|
||||
[Role.DOCTOR]: {
|
||||
role: Role.DOCTOR,
|
||||
phone: '13800001004',
|
||||
password: E2E_SEED_PASSWORD,
|
||||
hospitalId: 1,
|
||||
},
|
||||
[Role.ENGINEER]: {
|
||||
role: Role.ENGINEER,
|
||||
phone: '13800001005',
|
||||
password: E2E_SEED_PASSWORD,
|
||||
hospitalId: 1,
|
||||
},
|
||||
};
|
||||
41
test/e2e/helpers/e2e-app.helper.ts
Normal file
41
test/e2e/helpers/e2e-app.helper.ts
Normal file
@ -0,0 +1,41 @@
|
||||
import 'dotenv/config';
|
||||
import { BadRequestException, ValidationPipe } from '@nestjs/common';
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { AppModule } from '../../../src/app.module.js';
|
||||
import { HttpExceptionFilter } from '../../../src/common/http-exception.filter.js';
|
||||
import { MESSAGES } from '../../../src/common/messages.js';
|
||||
import { ResponseEnvelopeInterceptor } from '../../../src/common/response-envelope.interceptor.js';
|
||||
|
||||
export async function createE2eApp(): Promise<INestApplication> {
|
||||
const moduleRef = await Test.createTestingModule({
|
||||
imports: [AppModule],
|
||||
}).compile();
|
||||
|
||||
const app = moduleRef.createNestApplication();
|
||||
|
||||
app.useGlobalPipes(
|
||||
new ValidationPipe({
|
||||
whitelist: true,
|
||||
transform: true,
|
||||
forbidNonWhitelisted: true,
|
||||
exceptionFactory: (errors) => {
|
||||
const messages = errors
|
||||
.flatMap((error) => Object.values(error.constraints ?? {}))
|
||||
.filter((item): item is string => Boolean(item));
|
||||
|
||||
return new BadRequestException(
|
||||
messages.length > 0
|
||||
? messages.join(';')
|
||||
: MESSAGES.DEFAULT_BAD_REQUEST,
|
||||
);
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
app.useGlobalFilters(new HttpExceptionFilter());
|
||||
app.useGlobalInterceptors(new ResponseEnvelopeInterceptor());
|
||||
|
||||
await app.init();
|
||||
return app;
|
||||
}
|
||||
47
test/e2e/helpers/e2e-auth.helper.ts
Normal file
47
test/e2e/helpers/e2e-auth.helper.ts
Normal file
@ -0,0 +1,47 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import request from 'supertest';
|
||||
import {
|
||||
E2E_ROLE_LIST,
|
||||
type E2ERole,
|
||||
E2E_SEED_CREDENTIALS,
|
||||
} from '../fixtures/e2e-roles.js';
|
||||
import { expectSuccessEnvelope } from './e2e-http.helper.js';
|
||||
|
||||
export type E2EAccessTokenMap = Record<E2ERole, string>;
|
||||
|
||||
export async function loginAsRole(
|
||||
app: INestApplication,
|
||||
role: E2ERole,
|
||||
): Promise<string> {
|
||||
const credential = E2E_SEED_CREDENTIALS[role];
|
||||
const payload: Record<string, unknown> = {
|
||||
phone: credential.phone,
|
||||
password: credential.password,
|
||||
role: credential.role,
|
||||
};
|
||||
|
||||
if (credential.hospitalId != null) {
|
||||
payload.hospitalId = credential.hospitalId;
|
||||
}
|
||||
|
||||
const response = await request(app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send(payload);
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data?.accessToken).toEqual(expect.any(String));
|
||||
|
||||
return response.body.data.accessToken as string;
|
||||
}
|
||||
|
||||
export async function loginAllRoles(
|
||||
app: INestApplication,
|
||||
): Promise<E2EAccessTokenMap> {
|
||||
const tokenEntries = await Promise.all(
|
||||
E2E_ROLE_LIST.map(
|
||||
async (role) => [role, await loginAsRole(app, role)] as const,
|
||||
),
|
||||
);
|
||||
|
||||
return Object.fromEntries(tokenEntries) as E2EAccessTokenMap;
|
||||
}
|
||||
38
test/e2e/helpers/e2e-context.helper.ts
Normal file
38
test/e2e/helpers/e2e-context.helper.ts
Normal file
@ -0,0 +1,38 @@
|
||||
import type { INestApplication } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../src/prisma.service.js';
|
||||
import { loginAllRoles, type E2EAccessTokenMap } from './e2e-auth.helper.js';
|
||||
import { createE2eApp } from './e2e-app.helper.js';
|
||||
import {
|
||||
loadSeedFixtures,
|
||||
type E2ESeedFixtures,
|
||||
} from './e2e-fixtures.helper.js';
|
||||
|
||||
export interface E2EContext {
|
||||
app: INestApplication;
|
||||
prisma: PrismaService;
|
||||
tokens: E2EAccessTokenMap;
|
||||
fixtures: E2ESeedFixtures;
|
||||
}
|
||||
|
||||
export async function createE2EContext(): Promise<E2EContext> {
|
||||
const app = await createE2eApp();
|
||||
const prisma = app.get(PrismaService);
|
||||
const fixtures = await loadSeedFixtures(prisma);
|
||||
const tokens = await loginAllRoles(app);
|
||||
|
||||
return {
|
||||
app,
|
||||
prisma,
|
||||
fixtures,
|
||||
tokens,
|
||||
};
|
||||
}
|
||||
|
||||
export async function closeE2EContext(ctx?: E2EContext) {
|
||||
if (!ctx) {
|
||||
return;
|
||||
}
|
||||
|
||||
await ctx.prisma.$disconnect();
|
||||
await ctx.app.close();
|
||||
}
|
||||
195
test/e2e/helpers/e2e-fixtures.helper.ts
Normal file
195
test/e2e/helpers/e2e-fixtures.helper.ts
Normal file
@ -0,0 +1,195 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { PrismaService } from '../../../src/prisma.service.js';
|
||||
|
||||
export interface E2ESeedFixtures {
|
||||
hospitalAId: number;
|
||||
hospitalBId: number;
|
||||
departmentA1Id: number;
|
||||
departmentA2Id: number;
|
||||
departmentB1Id: number;
|
||||
groupA1Id: number;
|
||||
groupA2Id: number;
|
||||
groupB1Id: number;
|
||||
users: {
|
||||
systemAdminId: number;
|
||||
hospitalAdminAId: number;
|
||||
directorAId: number;
|
||||
leaderAId: number;
|
||||
doctorAId: number;
|
||||
doctorA2Id: number;
|
||||
doctorA3Id: number;
|
||||
doctorBId: number;
|
||||
engineerAId: number;
|
||||
engineerBId: number;
|
||||
};
|
||||
patients: {
|
||||
patientA1Id: number;
|
||||
patientA2Id: number;
|
||||
patientA3Id: number;
|
||||
patientB1Id: number;
|
||||
};
|
||||
devices: {
|
||||
deviceA1Id: number;
|
||||
deviceA2Id: number;
|
||||
deviceA3Id: number;
|
||||
deviceA4InactiveId: number;
|
||||
deviceB1Id: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface SeedUserScope {
|
||||
id: number;
|
||||
hospitalId: number | null;
|
||||
departmentId: number | null;
|
||||
groupId: number | null;
|
||||
}
|
||||
|
||||
async function requireUserScope(
|
||||
prisma: PrismaService,
|
||||
openId: string,
|
||||
): Promise<SeedUserScope> {
|
||||
const user = await prisma.user.findUnique({
|
||||
where: { openId },
|
||||
select: {
|
||||
id: true,
|
||||
hospitalId: true,
|
||||
departmentId: true,
|
||||
groupId: true,
|
||||
},
|
||||
});
|
||||
if (!user) {
|
||||
throw new NotFoundException(`Seed user not found: ${openId}`);
|
||||
}
|
||||
return user;
|
||||
}
|
||||
|
||||
async function requireDeviceId(
|
||||
prisma: PrismaService,
|
||||
snCode: string,
|
||||
): Promise<number> {
|
||||
const device = await prisma.device.findUnique({
|
||||
where: { snCode },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!device) {
|
||||
throw new NotFoundException(`Seed device not found: ${snCode}`);
|
||||
}
|
||||
return device.id;
|
||||
}
|
||||
|
||||
async function requirePatientId(
|
||||
prisma: PrismaService,
|
||||
hospitalId: number,
|
||||
phone: string,
|
||||
idCardHash: string,
|
||||
): Promise<number> {
|
||||
const patient = await prisma.patient.findFirst({
|
||||
where: { hospitalId, phone, idCardHash },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!patient) {
|
||||
throw new NotFoundException(
|
||||
`Seed patient not found: ${phone}/${idCardHash}`,
|
||||
);
|
||||
}
|
||||
return patient.id;
|
||||
}
|
||||
|
||||
export async function loadSeedFixtures(
|
||||
prisma: PrismaService,
|
||||
): Promise<E2ESeedFixtures> {
|
||||
const systemAdmin = await requireUserScope(
|
||||
prisma,
|
||||
'seed-system-admin-openid',
|
||||
);
|
||||
const hospitalAdminA = await requireUserScope(
|
||||
prisma,
|
||||
'seed-hospital-admin-a-openid',
|
||||
);
|
||||
const directorA = await requireUserScope(prisma, 'seed-director-a-openid');
|
||||
const leaderA = await requireUserScope(prisma, 'seed-leader-a-openid');
|
||||
const doctorA = await requireUserScope(prisma, 'seed-doctor-a-openid');
|
||||
const doctorA2 = await requireUserScope(prisma, 'seed-doctor-a2-openid');
|
||||
const doctorA3 = await requireUserScope(prisma, 'seed-doctor-a3-openid');
|
||||
const doctorB = await requireUserScope(prisma, 'seed-doctor-b-openid');
|
||||
const engineerA = await requireUserScope(prisma, 'seed-engineer-a-openid');
|
||||
const engineerB = await requireUserScope(prisma, 'seed-engineer-b-openid');
|
||||
|
||||
const hospitalAId = hospitalAdminA.hospitalId;
|
||||
const hospitalBId = doctorB.hospitalId;
|
||||
const departmentA1Id = doctorA.departmentId;
|
||||
const departmentA2Id = doctorA3.departmentId;
|
||||
const departmentB1Id = doctorB.departmentId;
|
||||
const groupA1Id = doctorA.groupId;
|
||||
const groupA2Id = doctorA3.groupId;
|
||||
const groupB1Id = doctorB.groupId;
|
||||
|
||||
if (
|
||||
hospitalAId == null ||
|
||||
hospitalBId == null ||
|
||||
departmentA1Id == null ||
|
||||
departmentA2Id == null ||
|
||||
departmentB1Id == null ||
|
||||
groupA1Id == null ||
|
||||
groupA2Id == null ||
|
||||
groupB1Id == null
|
||||
) {
|
||||
throw new NotFoundException('Seed user scope is incomplete');
|
||||
}
|
||||
|
||||
return {
|
||||
hospitalAId,
|
||||
hospitalBId,
|
||||
departmentA1Id,
|
||||
departmentA2Id,
|
||||
departmentB1Id,
|
||||
groupA1Id,
|
||||
groupA2Id,
|
||||
groupB1Id,
|
||||
users: {
|
||||
systemAdminId: systemAdmin.id,
|
||||
hospitalAdminAId: hospitalAdminA.id,
|
||||
directorAId: directorA.id,
|
||||
leaderAId: leaderA.id,
|
||||
doctorAId: doctorA.id,
|
||||
doctorA2Id: doctorA2.id,
|
||||
doctorA3Id: doctorA3.id,
|
||||
doctorBId: doctorB.id,
|
||||
engineerAId: engineerA.id,
|
||||
engineerBId: engineerB.id,
|
||||
},
|
||||
patients: {
|
||||
patientA1Id: await requirePatientId(
|
||||
prisma,
|
||||
hospitalAId,
|
||||
'13800002001',
|
||||
'seed-id-card-cross-hospital',
|
||||
),
|
||||
patientA2Id: await requirePatientId(
|
||||
prisma,
|
||||
hospitalAId,
|
||||
'13800002002',
|
||||
'seed-id-card-a2',
|
||||
),
|
||||
patientA3Id: await requirePatientId(
|
||||
prisma,
|
||||
hospitalAId,
|
||||
'13800002003',
|
||||
'seed-id-card-a3',
|
||||
),
|
||||
patientB1Id: await requirePatientId(
|
||||
prisma,
|
||||
hospitalBId,
|
||||
'13800002001',
|
||||
'seed-id-card-cross-hospital',
|
||||
),
|
||||
},
|
||||
devices: {
|
||||
deviceA1Id: await requireDeviceId(prisma, 'SEED-SN-A-001'),
|
||||
deviceA2Id: await requireDeviceId(prisma, 'SEED-SN-A-002'),
|
||||
deviceA3Id: await requireDeviceId(prisma, 'SEED-SN-A-003'),
|
||||
deviceA4InactiveId: await requireDeviceId(prisma, 'SEED-SN-A-004'),
|
||||
deviceB1Id: await requireDeviceId(prisma, 'SEED-SN-B-001'),
|
||||
},
|
||||
};
|
||||
}
|
||||
37
test/e2e/helpers/e2e-http.helper.ts
Normal file
37
test/e2e/helpers/e2e-http.helper.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import type { Response } from 'supertest';
|
||||
|
||||
export function expectSuccessEnvelope(response: Response, status: number) {
|
||||
expect(response.status).toBe(status);
|
||||
expect(response.body).toEqual(
|
||||
expect.objectContaining({
|
||||
code: 0,
|
||||
msg: '成功',
|
||||
}),
|
||||
);
|
||||
expect(response.body).toHaveProperty('data');
|
||||
}
|
||||
|
||||
export function expectErrorEnvelope(
|
||||
response: Response,
|
||||
status: number,
|
||||
messageIncludes?: string,
|
||||
) {
|
||||
expect(response.status).toBe(status);
|
||||
expect(response.body.code).toBe(status);
|
||||
expect(response.body.data).toBeNull();
|
||||
|
||||
if (messageIncludes) {
|
||||
expect(String(response.body.msg)).toContain(messageIncludes);
|
||||
}
|
||||
}
|
||||
|
||||
export function uniqueSeedValue(prefix: string): string {
|
||||
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
}
|
||||
|
||||
export function uniquePhone(): string {
|
||||
const suffix = `${Date.now()}${Math.floor(Math.random() * 1000)}`
|
||||
.replace(/\D/g, '')
|
||||
.slice(-10);
|
||||
return `1${suffix.padStart(10, '0')}`.slice(0, 11);
|
||||
}
|
||||
32
test/e2e/helpers/e2e-matrix.helper.ts
Normal file
32
test/e2e/helpers/e2e-matrix.helper.ts
Normal file
@ -0,0 +1,32 @@
|
||||
import type { Response } from 'supertest';
|
||||
import { E2E_ROLE_LIST, type E2ERole } from '../fixtures/e2e-roles.js';
|
||||
import type { E2EAccessTokenMap } from './e2e-auth.helper.js';
|
||||
|
||||
interface RoleMatrixCase {
|
||||
name: string;
|
||||
tokens: E2EAccessTokenMap;
|
||||
expectedStatusByRole: Record<E2ERole, number>;
|
||||
sendAsRole: (role: E2ERole, token: string) => Promise<Response>;
|
||||
sendWithoutToken: () => Promise<Response>;
|
||||
expectedStatusWithoutToken?: number;
|
||||
}
|
||||
|
||||
export async function assertRoleMatrix(matrixCase: RoleMatrixCase) {
|
||||
for (const role of E2E_ROLE_LIST) {
|
||||
const response = await matrixCase.sendAsRole(role, matrixCase.tokens[role]);
|
||||
const expectedStatus = matrixCase.expectedStatusByRole[role];
|
||||
const isSuccess = expectedStatus >= 200 && expectedStatus < 300;
|
||||
|
||||
expect(response.status).toBe(expectedStatus);
|
||||
expect(response.body.code).toBe(isSuccess ? 0 : expectedStatus);
|
||||
}
|
||||
|
||||
const unauthorizedResponse = await matrixCase.sendWithoutToken();
|
||||
const unauthorizedStatus = matrixCase.expectedStatusWithoutToken ?? 401;
|
||||
expect(unauthorizedResponse.status).toBe(unauthorizedStatus);
|
||||
expect(unauthorizedResponse.body.code).toBe(
|
||||
unauthorizedStatus >= 200 && unauthorizedStatus < 300
|
||||
? 0
|
||||
: unauthorizedStatus,
|
||||
);
|
||||
}
|
||||
129
test/e2e/specs/auth.e2e-spec.ts
Normal file
129
test/e2e/specs/auth.e2e-spec.ts
Normal file
@ -0,0 +1,129 @@
|
||||
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('AuthController (e2e)', () => {
|
||||
let ctx: E2EContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await createE2EContext();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeE2EContext(ctx);
|
||||
});
|
||||
|
||||
describe('POST /auth/register', () => {
|
||||
it('成功:注册医生账号', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
name: uniqueSeedValue('Auth 注册医生'),
|
||||
phone: uniquePhone(),
|
||||
password: 'Seed@1234',
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: ctx.fixtures.hospitalAId,
|
||||
departmentId: ctx.fixtures.departmentA1Id,
|
||||
groupId: ctx.fixtures.groupA1Id,
|
||||
openId: uniqueSeedValue('auth-register-openid'),
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data.role).toBe(Role.DOCTOR);
|
||||
});
|
||||
|
||||
it('失败:参数不合法返回 400', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/auth/register')
|
||||
.send({
|
||||
name: 'bad-register',
|
||||
phone: '13800009999',
|
||||
password: '123',
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: ctx.fixtures.hospitalAId,
|
||||
departmentId: ctx.fixtures.departmentA1Id,
|
||||
groupId: ctx.fixtures.groupA1Id,
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 400, 'password 长度至少 8 位');
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /auth/login', () => {
|
||||
it('成功:seed 账号登录并拿到 token', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
phone: '13800001004',
|
||||
password: 'Seed@1234',
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: ctx.fixtures.hospitalAId,
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data.accessToken).toEqual(expect.any(String));
|
||||
expect(response.body.data.actor.role).toBe(Role.DOCTOR);
|
||||
});
|
||||
|
||||
it('失败:密码错误返回 401', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/auth/login')
|
||||
.send({
|
||||
phone: '13800001004',
|
||||
password: 'Seed@12345',
|
||||
role: Role.DOCTOR,
|
||||
hospitalId: ctx.fixtures.hospitalAId,
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 401, '手机号、角色或密码错误');
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /auth/me', () => {
|
||||
it('成功:已登录用户可读取当前信息', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/auth/me')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data.role).toBe(Role.DOCTOR);
|
||||
});
|
||||
|
||||
it('失败:未登录返回 401', async () => {
|
||||
const response = await request(ctx.app.getHttpServer()).get('/auth/me');
|
||||
expectErrorEnvelope(response, 401, '缺少 Bearer Token');
|
||||
});
|
||||
|
||||
it('角色矩阵:6 角色都可访问,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /auth/me role matrix',
|
||||
tokens: ctx.tokens,
|
||||
expectedStatusByRole: {
|
||||
[Role.SYSTEM_ADMIN]: 200,
|
||||
[Role.HOSPITAL_ADMIN]: 200,
|
||||
[Role.DIRECTOR]: 200,
|
||||
[Role.LEADER]: 200,
|
||||
[Role.DOCTOR]: 200,
|
||||
[Role.ENGINEER]: 200,
|
||||
},
|
||||
sendAsRole: async (_role, token) =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.get('/auth/me')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get('/auth/me'),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
729
test/e2e/specs/organization.e2e-spec.ts
Normal file
729
test/e2e/specs/organization.e2e-spec.ts
Normal file
@ -0,0 +1,729 @@
|
||||
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,
|
||||
uniqueSeedValue,
|
||||
} from '../helpers/e2e-http.helper.js';
|
||||
|
||||
describe('Organization Controllers (e2e)', () => {
|
||||
let ctx: E2EContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await createE2EContext();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeE2EContext(ctx);
|
||||
});
|
||||
|
||||
describe('HospitalsController', () => {
|
||||
describe('POST /b/organization/hospitals', () => {
|
||||
it('成功:SYSTEM_ADMIN 可创建医院', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/hospitals')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
|
||||
.send({ name: uniqueSeedValue('组织-医院') });
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data.name).toContain('组织-医院');
|
||||
});
|
||||
|
||||
it('失败:非系统管理员创建返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/hospitals')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: uniqueSeedValue('组织-医院-失败') });
|
||||
|
||||
expectErrorEnvelope(response, 403, '无权限执行当前操作');
|
||||
});
|
||||
|
||||
it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/organization/hospitals 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())
|
||||
.post('/b/organization/hospitals')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({}),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/hospitals')
|
||||
.send({}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /b/organization/hospitals', () => {
|
||||
it('成功:SYSTEM_ADMIN 可查询医院列表', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/b/organization/hospitals')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data).toHaveProperty('list');
|
||||
});
|
||||
|
||||
it('失败:未登录返回 401', async () => {
|
||||
const response = await request(ctx.app.getHttpServer()).get(
|
||||
'/b/organization/hospitals',
|
||||
);
|
||||
expectErrorEnvelope(response, 401, '缺少 Bearer Token');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/organization/hospitals 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('/b/organization/hospitals')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get('/b/organization/hospitals'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /b/organization/hospitals/:id', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可查询本院详情', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data.id).toBe(ctx.fixtures.hospitalAId);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 查询他院返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get(`/b/organization/hospitals/${ctx.fixtures.hospitalBId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/organization/hospitals/: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(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`)
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get(
|
||||
`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`,
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /b/organization/hospitals/:id', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可更新本院名称', async () => {
|
||||
const originalName = 'Seed Hospital A';
|
||||
const nextName = uniqueSeedValue('医院更新');
|
||||
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: nextName });
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
|
||||
const rollbackResponse = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: originalName });
|
||||
expectSuccessEnvelope(rollbackResponse, 200);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 更新他院返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/hospitals/${ctx.fixtures.hospitalBId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: uniqueSeedValue('跨院更新失败') });
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'PATCH /b/organization/hospitals/: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('/b/organization/hospitals/99999999')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ name: 'matrix-hospital-patch' }),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.patch('/b/organization/hospitals/99999999')
|
||||
.send({ name: 'matrix-hospital-patch' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /b/organization/hospitals/:id', () => {
|
||||
it('成功:SYSTEM_ADMIN 可删除空医院', async () => {
|
||||
const createResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/hospitals')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
|
||||
.send({ name: uniqueSeedValue('医院待删') });
|
||||
expectSuccessEnvelope(createResponse, 201);
|
||||
|
||||
const targetId = createResponse.body.data.id as number;
|
||||
const deleteResponse = await request(ctx.app.getHttpServer())
|
||||
.delete(`/b/organization/hospitals/${targetId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(deleteResponse, 200);
|
||||
expect(deleteResponse.body.data.id).toBe(targetId);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 删除医院返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.delete(`/b/organization/hospitals/${ctx.fixtures.hospitalAId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(response, 403, '无权限执行当前操作');
|
||||
});
|
||||
|
||||
it('角色矩阵:仅 SYSTEM_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'DELETE /b/organization/hospitals/: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('/b/organization/hospitals/99999999')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).delete(
|
||||
'/b/organization/hospitals/99999999',
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DepartmentsController', () => {
|
||||
describe('POST /b/organization/departments', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可在本院创建科室', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/departments')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({
|
||||
name: uniqueSeedValue('组织-科室'),
|
||||
hospitalId: ctx.fixtures.hospitalAId,
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 跨院创建返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/departments')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({
|
||||
name: uniqueSeedValue('组织-跨院科室失败'),
|
||||
hospitalId: ctx.fixtures.hospitalBId,
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/organization/departments 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('/b/organization/departments')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({}),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/departments')
|
||||
.send({}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /b/organization/departments', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可查询本院科室列表', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/b/organization/departments')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data).toHaveProperty('list');
|
||||
});
|
||||
|
||||
it('失败:未登录返回 401', async () => {
|
||||
const response = await request(ctx.app.getHttpServer()).get(
|
||||
'/b/organization/departments',
|
||||
);
|
||||
expectErrorEnvelope(response, 401, '缺少 Bearer Token');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/organization/departments 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('/b/organization/departments')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get('/b/organization/departments'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /b/organization/departments/:id', () => {
|
||||
it('成功:SYSTEM_ADMIN 可查询科室详情', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get(`/b/organization/departments/${ctx.fixtures.departmentA1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data.id).toBe(ctx.fixtures.departmentA1Id);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 查询他院科室返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get(`/b/organization/departments/${ctx.fixtures.departmentB1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/organization/departments/: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(`/b/organization/departments/${ctx.fixtures.departmentA1Id}`)
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get(
|
||||
`/b/organization/departments/${ctx.fixtures.departmentA1Id}`,
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /b/organization/departments/:id', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可更新本院科室', async () => {
|
||||
const originalName = 'Cardiology-A2';
|
||||
const nextName = uniqueSeedValue('科室更新');
|
||||
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/departments/${ctx.fixtures.departmentA2Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: nextName });
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
|
||||
const rollbackResponse = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/departments/${ctx.fixtures.departmentA2Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: originalName });
|
||||
expectSuccessEnvelope(rollbackResponse, 200);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 更新他院科室返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/departments/${ctx.fixtures.departmentB1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: uniqueSeedValue('跨院科室更新失败') });
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'PATCH /b/organization/departments/: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('/b/organization/departments/99999999')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ name: 'matrix-department-patch' }),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.patch('/b/organization/departments/99999999')
|
||||
.send({ name: 'matrix-department-patch' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /b/organization/departments/:id', () => {
|
||||
it('成功:SYSTEM_ADMIN 可删除无关联科室', async () => {
|
||||
const createResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/departments')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
|
||||
.send({
|
||||
name: uniqueSeedValue('科室待删'),
|
||||
hospitalId: ctx.fixtures.hospitalAId,
|
||||
});
|
||||
expectSuccessEnvelope(createResponse, 201);
|
||||
|
||||
const targetId = createResponse.body.data.id as number;
|
||||
const deleteResponse = await request(ctx.app.getHttpServer())
|
||||
.delete(`/b/organization/departments/${targetId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(deleteResponse, 200);
|
||||
});
|
||||
|
||||
it('失败:存在关联数据删除返回 409', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.delete(`/b/organization/departments/${ctx.fixtures.departmentA1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(response, 409, '存在关联数据,无法删除');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'DELETE /b/organization/departments/: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('/b/organization/departments/99999999')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).delete(
|
||||
'/b/organization/departments/99999999',
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GroupsController', () => {
|
||||
describe('POST /b/organization/groups', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可创建小组', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/groups')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({
|
||||
name: uniqueSeedValue('组织-小组'),
|
||||
departmentId: ctx.fixtures.departmentA1Id,
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 跨院创建小组返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/groups')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({
|
||||
name: uniqueSeedValue('组织-跨院小组失败'),
|
||||
departmentId: ctx.fixtures.departmentB1Id,
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/organization/groups 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('/b/organization/groups')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({}),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/groups')
|
||||
.send({}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /b/organization/groups', () => {
|
||||
it('成功:SYSTEM_ADMIN 可查询小组列表', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/b/organization/groups')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data).toHaveProperty('list');
|
||||
});
|
||||
|
||||
it('失败:未登录返回 401', async () => {
|
||||
const response = await request(ctx.app.getHttpServer()).get(
|
||||
'/b/organization/groups',
|
||||
);
|
||||
expectErrorEnvelope(response, 401, '缺少 Bearer Token');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/organization/groups 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('/b/organization/groups')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get('/b/organization/groups'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /b/organization/groups/:id', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可查询本院小组详情', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get(`/b/organization/groups/${ctx.fixtures.groupA1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data.id).toBe(ctx.fixtures.groupA1Id);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 查询他院小组返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get(`/b/organization/groups/${ctx.fixtures.groupB1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/organization/groups/: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(`/b/organization/groups/${ctx.fixtures.groupA1Id}`)
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get(
|
||||
`/b/organization/groups/${ctx.fixtures.groupA1Id}`,
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('PATCH /b/organization/groups/:id', () => {
|
||||
it('成功:HOSPITAL_ADMIN 可更新本院小组', async () => {
|
||||
const originalName = 'Shift-A2';
|
||||
const nextName = uniqueSeedValue('小组更新');
|
||||
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/groups/${ctx.fixtures.groupA2Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: nextName });
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
|
||||
const rollbackResponse = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/groups/${ctx.fixtures.groupA2Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: originalName });
|
||||
expectSuccessEnvelope(rollbackResponse, 200);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 更新他院小组返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.patch(`/b/organization/groups/${ctx.fixtures.groupB1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`)
|
||||
.send({ name: uniqueSeedValue('跨院小组更新失败') });
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'PATCH /b/organization/groups/: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('/b/organization/groups/99999999')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ name: 'matrix-group-patch' }),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.patch('/b/organization/groups/99999999')
|
||||
.send({ name: 'matrix-group-patch' }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('DELETE /b/organization/groups/:id', () => {
|
||||
it('成功:SYSTEM_ADMIN 可删除无关联小组', async () => {
|
||||
const createResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/organization/groups')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
|
||||
.send({
|
||||
name: uniqueSeedValue('小组待删'),
|
||||
departmentId: ctx.fixtures.departmentA1Id,
|
||||
});
|
||||
expectSuccessEnvelope(createResponse, 201);
|
||||
|
||||
const targetId = createResponse.body.data.id as number;
|
||||
const deleteResponse = await request(ctx.app.getHttpServer())
|
||||
.delete(`/b/organization/groups/${targetId}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectSuccessEnvelope(deleteResponse, 200);
|
||||
});
|
||||
|
||||
it('失败:HOSPITAL_ADMIN 删除他院小组返回 403', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.delete(`/b/organization/groups/${ctx.fixtures.groupB1Id}`)
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(response, 403, '院管仅可操作本院组织数据');
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'DELETE /b/organization/groups/: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('/b/organization/groups/99999999')
|
||||
.set('Authorization', `Bearer ${token}`),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).delete(
|
||||
'/b/organization/groups/99999999',
|
||||
),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
185
test/e2e/specs/patients.e2e-spec.ts
Normal file
185
test/e2e/specs/patients.e2e-spec.ts
Normal file
@ -0,0 +1,185 @@
|
||||
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,
|
||||
} from '../helpers/e2e-http.helper.js';
|
||||
|
||||
describe('Patients Controllers (e2e)', () => {
|
||||
let ctx: E2EContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await createE2EContext();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeE2EContext(ctx);
|
||||
});
|
||||
|
||||
describe('GET /b/patients', () => {
|
||||
it('成功:按角色返回正确可见性范围', async () => {
|
||||
const systemAdminResponse = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.query({ hospitalId: ctx.fixtures.hospitalAId })
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
expectSuccessEnvelope(systemAdminResponse, 200);
|
||||
const systemPatientIds = (
|
||||
systemAdminResponse.body.data as Array<{ id: number }>
|
||||
).map((item) => item.id);
|
||||
expect(systemPatientIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
ctx.fixtures.patients.patientA1Id,
|
||||
ctx.fixtures.patients.patientA2Id,
|
||||
ctx.fixtures.patients.patientA3Id,
|
||||
]),
|
||||
);
|
||||
|
||||
const hospitalAdminResponse = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||||
expectSuccessEnvelope(hospitalAdminResponse, 200);
|
||||
const hospitalPatientIds = (
|
||||
hospitalAdminResponse.body.data as Array<{ id: number }>
|
||||
).map((item) => item.id);
|
||||
expect(hospitalPatientIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
ctx.fixtures.patients.patientA1Id,
|
||||
ctx.fixtures.patients.patientA2Id,
|
||||
ctx.fixtures.patients.patientA3Id,
|
||||
]),
|
||||
);
|
||||
|
||||
const directorResponse = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
|
||||
expectSuccessEnvelope(directorResponse, 200);
|
||||
const directorPatientIds = (
|
||||
directorResponse.body.data as Array<{ id: number }>
|
||||
).map((item) => item.id);
|
||||
expect(directorPatientIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
ctx.fixtures.patients.patientA1Id,
|
||||
ctx.fixtures.patients.patientA2Id,
|
||||
]),
|
||||
);
|
||||
expect(directorPatientIds).not.toContain(
|
||||
ctx.fixtures.patients.patientA3Id,
|
||||
);
|
||||
|
||||
const leaderResponse = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.LEADER]}`);
|
||||
expectSuccessEnvelope(leaderResponse, 200);
|
||||
const leaderPatientIds = (
|
||||
leaderResponse.body.data as Array<{ id: number }>
|
||||
).map((item) => item.id);
|
||||
expect(leaderPatientIds).toEqual(
|
||||
expect.arrayContaining([
|
||||
ctx.fixtures.patients.patientA1Id,
|
||||
ctx.fixtures.patients.patientA2Id,
|
||||
]),
|
||||
);
|
||||
expect(leaderPatientIds).not.toContain(ctx.fixtures.patients.patientA3Id);
|
||||
|
||||
const doctorResponse = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
|
||||
expectSuccessEnvelope(doctorResponse, 200);
|
||||
const doctorPatientIds = (
|
||||
doctorResponse.body.data as Array<{ id: number }>
|
||||
).map((item) => item.id);
|
||||
expect(doctorPatientIds).toContain(ctx.fixtures.patients.patientA1Id);
|
||||
expect(doctorPatientIds).not.toContain(ctx.fixtures.patients.patientA2Id);
|
||||
expect(doctorPatientIds).not.toContain(ctx.fixtures.patients.patientA3Id);
|
||||
|
||||
const engineerResponse = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`);
|
||||
expectErrorEnvelope(engineerResponse, 403, '无权限执行当前操作');
|
||||
});
|
||||
|
||||
it('失败:SYSTEM_ADMIN 不传 hospitalId 返回 400', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||||
|
||||
expectErrorEnvelope(
|
||||
response,
|
||||
400,
|
||||
'系统管理员查询必须显式传入 hospitalId',
|
||||
);
|
||||
});
|
||||
|
||||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR/LEADER/DOCTOR 可访问,ENGINEER 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'GET /b/patients role matrix',
|
||||
tokens: ctx.tokens,
|
||||
expectedStatusByRole: {
|
||||
[Role.SYSTEM_ADMIN]: 200,
|
||||
[Role.HOSPITAL_ADMIN]: 200,
|
||||
[Role.DIRECTOR]: 200,
|
||||
[Role.LEADER]: 200,
|
||||
[Role.DOCTOR]: 200,
|
||||
[Role.ENGINEER]: 403,
|
||||
},
|
||||
sendAsRole: async (role, token) => {
|
||||
const req = request(ctx.app.getHttpServer())
|
||||
.get('/b/patients')
|
||||
.set('Authorization', `Bearer ${token}`);
|
||||
|
||||
if (role === Role.SYSTEM_ADMIN) {
|
||||
req.query({ hospitalId: ctx.fixtures.hospitalAId });
|
||||
}
|
||||
|
||||
return req;
|
||||
},
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).get('/b/patients'),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('GET /c/patients/lifecycle', () => {
|
||||
it('成功:可按 phone + idCardHash 查询跨院生命周期', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/c/patients/lifecycle')
|
||||
.query({
|
||||
phone: '13800002001',
|
||||
idCardHash: 'seed-id-card-cross-hospital',
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 200);
|
||||
expect(response.body.data.phone).toBe('13800002001');
|
||||
expect(response.body.data.idCardHash).toBe('seed-id-card-cross-hospital');
|
||||
expect(response.body.data.patientCount).toBeGreaterThanOrEqual(2);
|
||||
expect(Array.isArray(response.body.data.lifecycle)).toBe(true);
|
||||
});
|
||||
|
||||
it('失败:参数缺失返回 400', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/c/patients/lifecycle')
|
||||
.query({
|
||||
phone: '13800002001',
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 400, 'idCardHash 必须是字符串');
|
||||
});
|
||||
|
||||
it('失败:不存在患者返回 404', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.get('/c/patients/lifecycle')
|
||||
.query({
|
||||
phone: '13800009999',
|
||||
idCardHash: 'not-exists-idcard-hash',
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 404, '未找到匹配的患者档案');
|
||||
});
|
||||
});
|
||||
});
|
||||
325
test/e2e/specs/tasks.e2e-spec.ts
Normal file
325
test/e2e/specs/tasks.e2e-spec.ts
Normal file
@ -0,0 +1,325 @@
|
||||
import request from 'supertest';
|
||||
import { Role, TaskStatus } 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,
|
||||
} from '../helpers/e2e-http.helper.js';
|
||||
|
||||
describe('BTasksController (e2e)', () => {
|
||||
let ctx: E2EContext;
|
||||
|
||||
beforeAll(async () => {
|
||||
ctx = await createE2EContext();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await closeE2EContext(ctx);
|
||||
});
|
||||
|
||||
async function publishPendingTask(deviceId: number, targetPressure: number) {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/publish')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
deviceId,
|
||||
targetPressure,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
return response.body.data as { id: number; status: TaskStatus };
|
||||
}
|
||||
|
||||
describe('POST /b/tasks/publish', () => {
|
||||
it('成功:DOCTOR 可发布任务', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/publish')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
|
||||
.send({
|
||||
engineerId: ctx.fixtures.users.engineerAId,
|
||||
items: [
|
||||
{
|
||||
deviceId: ctx.fixtures.devices.deviceA2Id,
|
||||
targetPressure: 126,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data.status).toBe(TaskStatus.PENDING);
|
||||
});
|
||||
|
||||
it('失败:发布跨院设备任务返回 404', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/publish')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
|
||||
.send({
|
||||
items: [
|
||||
{
|
||||
deviceId: ctx.fixtures.devices.deviceB1Id,
|
||||
targetPressure: 120,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
expectErrorEnvelope(response, 404, '存在设备不在当前医院或设备不存在');
|
||||
});
|
||||
|
||||
it('角色矩阵:仅 DOCTOR 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/tasks/publish role matrix',
|
||||
tokens: ctx.tokens,
|
||||
expectedStatusByRole: {
|
||||
[Role.SYSTEM_ADMIN]: 403,
|
||||
[Role.HOSPITAL_ADMIN]: 403,
|
||||
[Role.DIRECTOR]: 403,
|
||||
[Role.LEADER]: 403,
|
||||
[Role.DOCTOR]: 400,
|
||||
[Role.ENGINEER]: 403,
|
||||
},
|
||||
sendAsRole: async (_role, token) =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/publish')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({}),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer()).post('/b/tasks/publish').send({}),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /b/tasks/accept', () => {
|
||||
it('成功:ENGINEER 可接收待处理任务', async () => {
|
||||
const task = await publishPendingTask(
|
||||
ctx.fixtures.devices.deviceA2Id,
|
||||
127,
|
||||
);
|
||||
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data.status).toBe(TaskStatus.ACCEPTED);
|
||||
expect(response.body.data.engineerId).toBe(
|
||||
ctx.fixtures.users.engineerAId,
|
||||
);
|
||||
});
|
||||
|
||||
it('失败:接收不存在任务返回 404', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: 99999999 });
|
||||
|
||||
expectErrorEnvelope(response, 404, '任务不存在或不属于当前医院');
|
||||
});
|
||||
|
||||
it('状态机失败:重复接收返回 409', async () => {
|
||||
const task = await publishPendingTask(
|
||||
ctx.fixtures.devices.deviceA3Id,
|
||||
122,
|
||||
);
|
||||
|
||||
const firstAccept = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
expectSuccessEnvelope(firstAccept, 201);
|
||||
|
||||
const secondAccept = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
|
||||
expectErrorEnvelope(secondAccept, 409, '仅待接收任务可执行接收');
|
||||
});
|
||||
|
||||
it('角色矩阵:仅 ENGINEER 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/tasks/accept role matrix',
|
||||
tokens: ctx.tokens,
|
||||
expectedStatusByRole: {
|
||||
[Role.SYSTEM_ADMIN]: 403,
|
||||
[Role.HOSPITAL_ADMIN]: 403,
|
||||
[Role.DIRECTOR]: 403,
|
||||
[Role.LEADER]: 403,
|
||||
[Role.DOCTOR]: 403,
|
||||
[Role.ENGINEER]: 404,
|
||||
},
|
||||
sendAsRole: async (_role, token) =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ taskId: 99999999 }),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.send({ taskId: 99999999 }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /b/tasks/complete', () => {
|
||||
it('成功:ENGINEER 完成已接收任务并同步设备压力', async () => {
|
||||
const targetPressure = 135;
|
||||
const task = await publishPendingTask(
|
||||
ctx.fixtures.devices.deviceA1Id,
|
||||
targetPressure,
|
||||
);
|
||||
|
||||
const acceptResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
expectSuccessEnvelope(acceptResponse, 201);
|
||||
|
||||
const completeResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/complete')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
|
||||
expectSuccessEnvelope(completeResponse, 201);
|
||||
expect(completeResponse.body.data.status).toBe(TaskStatus.COMPLETED);
|
||||
|
||||
const device = await ctx.prisma.device.findUnique({
|
||||
where: { id: ctx.fixtures.devices.deviceA1Id },
|
||||
select: { currentPressure: true },
|
||||
});
|
||||
expect(device?.currentPressure).toBe(targetPressure);
|
||||
});
|
||||
|
||||
it('失败:完成不存在任务返回 404', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/complete')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: 99999999 });
|
||||
|
||||
expectErrorEnvelope(response, 404, '任务不存在或不属于当前医院');
|
||||
});
|
||||
|
||||
it('状态机失败:未接收任务直接完成返回 409', async () => {
|
||||
const task = await publishPendingTask(
|
||||
ctx.fixtures.devices.deviceA2Id,
|
||||
124,
|
||||
);
|
||||
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/complete')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
|
||||
expectErrorEnvelope(response, 409, '仅已接收任务可执行完成');
|
||||
});
|
||||
|
||||
it('角色矩阵:仅 ENGINEER 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/tasks/complete role matrix',
|
||||
tokens: ctx.tokens,
|
||||
expectedStatusByRole: {
|
||||
[Role.SYSTEM_ADMIN]: 403,
|
||||
[Role.HOSPITAL_ADMIN]: 403,
|
||||
[Role.DIRECTOR]: 403,
|
||||
[Role.LEADER]: 403,
|
||||
[Role.DOCTOR]: 403,
|
||||
[Role.ENGINEER]: 404,
|
||||
},
|
||||
sendAsRole: async (_role, token) =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/complete')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ taskId: 99999999 }),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/complete')
|
||||
.send({ taskId: 99999999 }),
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('POST /b/tasks/cancel', () => {
|
||||
it('成功:DOCTOR 可取消自己创建的任务', async () => {
|
||||
const task = await publishPendingTask(
|
||||
ctx.fixtures.devices.deviceA3Id,
|
||||
120,
|
||||
);
|
||||
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/cancel')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
|
||||
.send({ taskId: task.id });
|
||||
|
||||
expectSuccessEnvelope(response, 201);
|
||||
expect(response.body.data.status).toBe(TaskStatus.CANCELLED);
|
||||
});
|
||||
|
||||
it('失败:取消不存在任务返回 404', async () => {
|
||||
const response = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/cancel')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
|
||||
.send({ taskId: 99999999 });
|
||||
|
||||
expectErrorEnvelope(response, 404, '任务不存在或不属于当前医院');
|
||||
});
|
||||
|
||||
it('状态机失败:已完成任务不可取消返回 409', async () => {
|
||||
const task = await publishPendingTask(
|
||||
ctx.fixtures.devices.deviceA2Id,
|
||||
123,
|
||||
);
|
||||
|
||||
const acceptResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/accept')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
expectSuccessEnvelope(acceptResponse, 201);
|
||||
|
||||
const completeResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/complete')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`)
|
||||
.send({ taskId: task.id });
|
||||
expectSuccessEnvelope(completeResponse, 201);
|
||||
|
||||
const cancelResponse = await request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/cancel')
|
||||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`)
|
||||
.send({ taskId: task.id });
|
||||
|
||||
expectErrorEnvelope(cancelResponse, 409, '仅待接收/已接收任务可取消');
|
||||
});
|
||||
|
||||
it('角色矩阵:仅 DOCTOR 可进入业务,其他角色 403,未登录 401', async () => {
|
||||
await assertRoleMatrix({
|
||||
name: 'POST /b/tasks/cancel role matrix',
|
||||
tokens: ctx.tokens,
|
||||
expectedStatusByRole: {
|
||||
[Role.SYSTEM_ADMIN]: 403,
|
||||
[Role.HOSPITAL_ADMIN]: 403,
|
||||
[Role.DIRECTOR]: 403,
|
||||
[Role.LEADER]: 403,
|
||||
[Role.DOCTOR]: 404,
|
||||
[Role.ENGINEER]: 403,
|
||||
},
|
||||
sendAsRole: async (_role, token) =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/cancel')
|
||||
.set('Authorization', `Bearer ${token}`)
|
||||
.send({ taskId: 99999999 }),
|
||||
sendWithoutToken: async () =>
|
||||
request(ctx.app.getHttpServer())
|
||||
.post('/b/tasks/cancel')
|
||||
.send({ taskId: 99999999 }),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
332
test/e2e/specs/users.e2e-spec.ts
Normal file
332
test/e2e/specs/users.e2e-spec.ts
Normal file
@ -0,0 +1,332 @@
|
||||
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({}),
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
20
test/jest-e2e.config.cjs
Normal file
20
test/jest-e2e.config.cjs
Normal file
@ -0,0 +1,20 @@
|
||||
module.exports = {
|
||||
rootDir: '../',
|
||||
testEnvironment: 'node',
|
||||
moduleFileExtensions: ['js', 'json', 'ts'],
|
||||
testRegex: 'test/e2e/specs/.*\\.e2e-spec\\.ts$',
|
||||
transform: {
|
||||
'^.+\\.(t|j)s$': [
|
||||
'ts-jest',
|
||||
{
|
||||
useESM: true,
|
||||
tsconfig: '<rootDir>/test/tsconfig.e2e.json',
|
||||
},
|
||||
],
|
||||
},
|
||||
extensionsToTreatAsEsm: ['.ts'],
|
||||
moduleNameMapper: {
|
||||
'^(\\.{1,2}/.*)\\.js$': '$1',
|
||||
},
|
||||
maxWorkers: 1,
|
||||
};
|
||||
10
test/tsconfig.e2e.json
Normal file
10
test/tsconfig.e2e.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"extends": "../tsconfig.json",
|
||||
"compilerOptions": {
|
||||
"module": "nodenext",
|
||||
"moduleResolution": "nodenext",
|
||||
"types": ["node", "jest"],
|
||||
"noEmit": true
|
||||
},
|
||||
"include": ["./e2e/**/*.ts", "../src/**/*.ts"]
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user