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 }), }); }); }); });