修复 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": "nest start",
|
||||||
"start:dev": "nest start --watch",
|
"start:dev": "nest start --watch",
|
||||||
"start:debug": "nest start --debug --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": {
|
"dependencies": {
|
||||||
"@nestjs/common": "^11.0.1",
|
"@nestjs/common": "^11.0.1",
|
||||||
@ -39,14 +42,17 @@
|
|||||||
"@nestjs/testing": "^11.0.1",
|
"@nestjs/testing": "^11.0.1",
|
||||||
"@types/bcrypt": "^6.0.0",
|
"@types/bcrypt": "^6.0.0",
|
||||||
"@types/express": "^5.0.0",
|
"@types/express": "^5.0.0",
|
||||||
|
"@types/jest": "^30.0.0",
|
||||||
"@types/jsonwebtoken": "^9.0.10",
|
"@types/jsonwebtoken": "^9.0.10",
|
||||||
"@types/node": "^22.10.7",
|
"@types/node": "^22.10.7",
|
||||||
"@types/supertest": "^6.0.2",
|
"@types/supertest": "^6.0.2",
|
||||||
"globals": "^16.0.0",
|
"globals": "^16.0.0",
|
||||||
|
"jest": "^30.3.0",
|
||||||
"prettier": "^3.4.2",
|
"prettier": "^3.4.2",
|
||||||
"prisma": "^7.4.2",
|
"prisma": "^7.4.2",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"supertest": "^7.0.0",
|
"supertest": "^7.0.0",
|
||||||
|
"ts-jest": "^29.4.6",
|
||||||
"ts-loader": "^9.5.2",
|
"ts-loader": "^9.5.2",
|
||||||
"ts-node": "^10.9.2",
|
"ts-node": "^10.9.2",
|
||||||
"tsconfig-paths": "^4.2.0",
|
"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"
|
output = "../src/generated/prisma"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 兼容 seed 脚本在 Node.js 直接运行时使用 @prisma/client runtime。
|
||||||
|
generator seed_client {
|
||||||
|
provider = "prisma-client-js"
|
||||||
|
}
|
||||||
|
|
||||||
datasource db {
|
datasource db {
|
||||||
provider = "postgresql"
|
provider = "postgresql"
|
||||||
}
|
}
|
||||||
|
|||||||
519
prisma/seed.mjs
519
prisma/seed.mjs
@ -1,10 +1,10 @@
|
|||||||
import 'dotenv/config';
|
import 'dotenv/config';
|
||||||
import { PrismaPg } from '@prisma/adapter-pg';
|
import { PrismaPg } from '@prisma/adapter-pg';
|
||||||
import { hash } from 'bcrypt';
|
import { hash } from 'bcrypt';
|
||||||
import { PrismaClient } from '../src/generated/prisma/client.js';
|
import prismaClientPackage from '@prisma/client';
|
||||||
import { DeviceStatus, Role } from '../src/generated/prisma/enums.js';
|
|
||||||
|
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;
|
const connectionString = process.env.DATABASE_URL;
|
||||||
if (!connectionString) {
|
if (!connectionString) {
|
||||||
throw new Error('DATABASE_URL is required to run seed');
|
throw new Error('DATABASE_URL is required to run seed');
|
||||||
@ -14,177 +14,424 @@ const prisma = new PrismaClient({
|
|||||||
adapter: new PrismaPg({ connectionString }),
|
adapter: new PrismaPg({ connectionString }),
|
||||||
});
|
});
|
||||||
|
|
||||||
async function main() {
|
const SEED_PASSWORD_PLAIN = 'Seed@1234';
|
||||||
// Default seed login password (plain): Seed@1234
|
|
||||||
const seedPasswordHash = await hash('Seed@1234', 12);
|
|
||||||
|
|
||||||
// Seed a baseline organization tree for local/demo usage.
|
async function ensureHospital(name) {
|
||||||
const hospital =
|
return (
|
||||||
(await prisma.hospital.findFirst({ where: { name: 'Demo Hospital' } })) ??
|
(await prisma.hospital.findFirst({ where: { name } })) ??
|
||||||
(await prisma.hospital.create({
|
prisma.hospital.create({ data: { name } })
|
||||||
data: { name: 'Demo Hospital' },
|
);
|
||||||
}));
|
}
|
||||||
|
|
||||||
const department =
|
async function ensureDepartment(hospitalId, name) {
|
||||||
|
return (
|
||||||
(await prisma.department.findFirst({
|
(await prisma.department.findFirst({
|
||||||
where: {
|
where: { hospitalId, name },
|
||||||
hospitalId: hospital.id,
|
|
||||||
name: 'Neurosurgery',
|
|
||||||
},
|
|
||||||
})) ??
|
})) ??
|
||||||
(await prisma.department.create({
|
prisma.department.create({
|
||||||
data: {
|
data: { hospitalId, name },
|
||||||
hospitalId: hospital.id,
|
})
|
||||||
name: 'Neurosurgery',
|
);
|
||||||
},
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
const group =
|
async function ensureGroup(departmentId, name) {
|
||||||
|
return (
|
||||||
(await prisma.group.findFirst({
|
(await prisma.group.findFirst({
|
||||||
where: {
|
where: { departmentId, name },
|
||||||
departmentId: department.id,
|
|
||||||
name: 'Shift-A',
|
|
||||||
},
|
|
||||||
})) ??
|
})) ??
|
||||||
(await prisma.group.create({
|
prisma.group.create({
|
||||||
data: {
|
data: { departmentId, name },
|
||||||
departmentId: department.id,
|
})
|
||||||
name: 'Shift-A',
|
);
|
||||||
},
|
}
|
||||||
}));
|
|
||||||
|
|
||||||
// Use openId as idempotent unique key for seeded users.
|
async function upsertUserByOpenId(openId, data) {
|
||||||
const systemAdmin = await prisma.user.upsert({
|
return prisma.user.upsert({
|
||||||
where: { openId: 'seed-system-admin-openid' },
|
where: { openId },
|
||||||
update: {
|
update: data,
|
||||||
name: 'System Admin',
|
create: {
|
||||||
phone: '13800000000',
|
...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,
|
passwordHash: seedPasswordHash,
|
||||||
role: Role.SYSTEM_ADMIN,
|
role: Role.SYSTEM_ADMIN,
|
||||||
hospitalId: null,
|
hospitalId: null,
|
||||||
departmentId: null,
|
departmentId: null,
|
||||||
groupId: null,
|
groupId: null,
|
||||||
},
|
|
||||||
create: {
|
|
||||||
name: 'System Admin',
|
|
||||||
phone: '13800000000',
|
|
||||||
passwordHash: seedPasswordHash,
|
|
||||||
openId: 'seed-system-admin-openid',
|
|
||||||
role: Role.SYSTEM_ADMIN,
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
|
|
||||||
await prisma.user.upsert({
|
const hospitalAdminA = await upsertUserByOpenId(
|
||||||
where: { openId: 'seed-hospital-admin-openid' },
|
'seed-hospital-admin-a-openid',
|
||||||
update: {
|
{
|
||||||
name: 'Hospital Admin',
|
name: 'Seed Hospital Admin A',
|
||||||
phone: '13800000001',
|
phone: '13800001001',
|
||||||
passwordHash: seedPasswordHash,
|
passwordHash: seedPasswordHash,
|
||||||
role: Role.HOSPITAL_ADMIN,
|
role: Role.HOSPITAL_ADMIN,
|
||||||
hospitalId: hospital.id,
|
hospitalId: hospitalA.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,
|
|
||||||
departmentId: null,
|
departmentId: null,
|
||||||
groupId: 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,
|
passwordHash: seedPasswordHash,
|
||||||
openId: 'seed-engineer-openid',
|
|
||||||
role: Role.ENGINEER,
|
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 =
|
const deviceA2 = await prisma.device.upsert({
|
||||||
(await prisma.patient.findFirst({
|
where: { snCode: 'SEED-SN-A-002' },
|
||||||
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' },
|
|
||||||
update: {
|
update: {
|
||||||
patientId: patient.id,
|
patientId: patientA2.id,
|
||||||
currentPressure: 110,
|
currentPressure: 112,
|
||||||
status: DeviceStatus.ACTIVE,
|
status: DeviceStatus.ACTIVE,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
snCode: 'SEED-SN-001',
|
snCode: 'SEED-SN-A-002',
|
||||||
patientId: patient.id,
|
patientId: patientA2.id,
|
||||||
currentPressure: 110,
|
currentPressure: 112,
|
||||||
status: DeviceStatus.ACTIVE,
|
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(
|
console.log(
|
||||||
JSON.stringify(
|
JSON.stringify(
|
||||||
{
|
{
|
||||||
ok: true,
|
ok: true,
|
||||||
hospitalId: hospital.id,
|
seedPasswordPlain: SEED_PASSWORD_PLAIN,
|
||||||
departmentId: department.id,
|
hospitals: {
|
||||||
groupId: group.id,
|
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,
|
systemAdminId: systemAdmin.id,
|
||||||
doctorId: doctor.id,
|
hospitalAdminAId: hospitalAdminA.id,
|
||||||
patientId: patient.id,
|
directorAId: directorA.id,
|
||||||
seedPasswordPlain: 'Seed@1234',
|
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,
|
null,
|
||||||
2,
|
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