鉴权改为登录态回库校验,新增 tokenValidAfter 失效时间,支持密码变更与 seed 重置后旧 token 立即失效 患者字段由 idCardHash 统一迁移为 idCard,新增身份证标准化逻辑并同步 C 端生命周期查询参数 组织模块增加小组删除限制(有成员时返回 409)并补充中文错误消息 任务取消接口支持可选 reason 字段(先透传事件层) 补齐 Prisma 迁移、文档说明和 E2E 用例(含设备模块与 token 失效场景)
326 lines
11 KiB
TypeScript
326 lines
11 KiB
TypeScript
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/DIRECTOR/LEADER 可进入业务,其余角色 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]: 400,
|
||
[Role.LEADER]: 400,
|
||
[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/DIRECTOR/LEADER 可进入业务,其余角色 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]: 404,
|
||
[Role.LEADER]: 404,
|
||
[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 }),
|
||
});
|
||
});
|
||
});
|
||
});
|