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 publishAssignedTask( deviceId: number, targetPressure: string, actorToken = ctx.tokens[Role.DOCTOR], engineerId = ctx.fixtures.users.engineerAId, ) { const response = await request(ctx.app.getHttpServer()) .post('/b/tasks/publish') .set('Authorization', `Bearer ${actorToken}`) .send({ engineerId, items: [ { deviceId, targetPressure, }, ], }); expectSuccessEnvelope(response, 201); return response.body.data as { id: number; status: TaskStatus; engineerId: number; hospitalId: number; }; } describe('GET /b/tasks/engineers', () => { it('成功:DOCTOR 可查看本院可选工程师', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/tasks/engineers') .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`); expectSuccessEnvelope(response, 200); expect(Array.isArray(response.body.data)).toBe(true); expect(response.body.data).toEqual( expect.arrayContaining([ expect.objectContaining({ id: ctx.fixtures.users.engineerAId, }), ]), ); }); it('成功:SYSTEM_ADMIN 可按医院筛选工程师', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/tasks/engineers') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .query({ hospitalId: ctx.fixtures.hospitalBId }); expectSuccessEnvelope(response, 200); expect(response.body.data).toEqual([ expect.objectContaining({ id: ctx.fixtures.users.engineerBId, hospitalId: ctx.fixtures.hospitalBId, }), ]); }); it('失败:hospitalId 非法返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/tasks/engineers') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .query({ hospitalId: 0 }); expectErrorEnvelope(response, 400, 'hospitalId 必须大于 0'); }); }); describe('GET /b/tasks', () => { it('成功:SYSTEM_ADMIN 可查看跨医院调压记录', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/tasks') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .query({ page: 1, pageSize: 20 }); expectSuccessEnvelope(response, 200); expect(Array.isArray(response.body.data.list)).toBe(true); expect(response.body.data.total).toBeGreaterThan(0); expect( response.body.data.list.every( (item: { creator?: { id?: number; name?: string }; engineer?: { id?: number; name?: string }; }) => Number.isInteger(item.creator?.id) && Boolean(item.creator?.name) && Number.isInteger(item.engineer?.id) && Boolean(item.engineer?.name), ), ).toBe(true); const hospitalIds = Array.from( new Set( response.body.data.list .map((item: { hospital?: { id?: number } }) => item.hospital?.id) .filter(Boolean), ), ); expect(hospitalIds).toEqual( expect.arrayContaining([ ctx.fixtures.hospitalAId, ctx.fixtures.hospitalBId, ]), ); }); it('成功:DOCTOR 仅可查看本院调压记录', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/tasks') .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`) .query({ page: 1, pageSize: 20 }); expectSuccessEnvelope(response, 200); expect(Array.isArray(response.body.data.list)).toBe(true); expect(response.body.data.total).toBeGreaterThan(0); expect( response.body.data.list.every( (item: { hospital?: { id?: number } }) => item.hospital?.id === ctx.fixtures.hospitalAId, ), ).toBe(true); }); it('失败:hospitalId 非法返回 400', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/tasks') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .query({ hospitalId: 0 }); expectErrorEnvelope(response, 400, 'hospitalId 必须大于 0'); }); }); 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: '1.5', }, ], }); expectSuccessEnvelope(response, 201); expect(response.body.data.status).toBe(TaskStatus.ACCEPTED); expect(response.body.data.engineerId).toBe( ctx.fixtures.users.engineerAId, ); }); it('成功:SYSTEM_ADMIN 可按设备自动归院发布任务', async () => { const task = await publishAssignedTask( ctx.fixtures.devices.deviceA1Id, '1.5', ctx.tokens[Role.SYSTEM_ADMIN], ); expect(task.status).toBe(TaskStatus.ACCEPTED); expect(task.hospitalId).toBe(ctx.fixtures.hospitalAId); }); it('失败:未指定接收工程师返回 400', 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.deviceA2Id, targetPressure: '1.5', }, ], }); expectErrorEnvelope(response, 400, 'engineerId 必须是整数'); }); it('失败:可调压设备使用非法挡位返回 400', 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: '2', }, ], }); expectErrorEnvelope(response, 400, '压力值不在该植入物配置的挡位范围内'); }); it('失败:发布跨院设备任务返回 404', 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.deviceB1Id, targetPressure: '1.5', }, ], }); 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]: 400, [Role.HOSPITAL_ADMIN]: 400, [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 接收接口已停用,返回 403', async () => { const task = await publishAssignedTask( ctx.fixtures.devices.deviceA2Id, '1.5', ); const response = await request(ctx.app.getHttpServer()) .post('/b/tasks/accept') .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) .send({ taskId: task.id }); expectErrorEnvelope( response, 403, '当前流程不支持工程师接收,请由创建人直接指定接收工程师', ); }); it('角色矩阵:接收接口对所有角色都不可用,未登录 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]: 403, }, 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 = '1.5'; const task = await publishAssignedTask( ctx.fixtures.devices.deviceA1Id, targetPressure, ); 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 publishAssignedTask( ctx.fixtures.devices.deviceA2Id, '1', ); const firstComplete = await request(ctx.app.getHttpServer()) .post('/b/tasks/complete') .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) .send({ taskId: task.id }); expectSuccessEnvelope(firstComplete, 201); const secondComplete = await request(ctx.app.getHttpServer()) .post('/b/tasks/complete') .set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`) .send({ taskId: task.id }); expectErrorEnvelope(secondComplete, 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 publishAssignedTask( ctx.fixtures.devices.deviceA3Id, '1.5', ); 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 publishAssignedTask( ctx.fixtures.devices.deviceA2Id, '1.5', ); 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]: 404, [Role.HOSPITAL_ADMIN]: 404, [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 }), }); }); }); });