import request from 'supertest'; import { DeviceStatus, 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('BDevicesController (e2e)', () => { let ctx: E2EContext; beforeAll(async () => { ctx = await createE2EContext(); }); afterAll(async () => { await closeE2EContext(ctx); }); async function createDevice(token: string, patientId: number) { const response = await request(ctx.app.getHttpServer()) .post('/b/devices') .set('Authorization', `Bearer ${token}`) .send({ status: DeviceStatus.ACTIVE, patientId, }); expectSuccessEnvelope(response, 201); return response.body.data as { id: number; currentPressure: string; status: DeviceStatus; patient: { id: number }; }; } describe('GET /b/devices', () => { it('成功:SYSTEM_ADMIN 可分页查询设备列表', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/devices') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectSuccessEnvelope(response, 200); expect(Array.isArray(response.body.data.list)).toBe(true); expect(response.body.data.total).toBeGreaterThan(0); }); it('成功:HOSPITAL_ADMIN 仅能看到本院设备', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/devices') .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`); expectSuccessEnvelope(response, 200); const hospitalIds = ( response.body.data.list as Array<{ patient?: { hospital?: { id: number } }; }> ) .map((item) => item.patient?.hospital?.id) .filter(Boolean); expect(hospitalIds.every((id) => id === ctx.fixtures.hospitalAId)).toBe( true, ); }); it('角色矩阵:仅 SYSTEM_ADMIN/HOSPITAL_ADMIN 可访问列表,其他角色 403,未登录 401', async () => { await assertRoleMatrix({ name: 'GET /b/devices 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/devices') .set('Authorization', `Bearer ${token}`), sendWithoutToken: async () => request(ctx.app.getHttpServer()).get('/b/devices'), }); }); }); describe('植入物型号字典', () => { it('成功:DOCTOR 可查询可见型号字典', async () => { const response = await request(ctx.app.getHttpServer()) .get('/b/devices/catalogs') .set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`); expectSuccessEnvelope(response, 200); expect( (response.body.data as Array<{ modelCode: string }>).some( (item) => item.modelCode === 'SEED-ADJUSTABLE-VALVE', ), ).toBe(true); }); it('成功:SYSTEM_ADMIN 可新增、更新并删除全局植入物目录', async () => { const createResponse = await request(ctx.app.getHttpServer()) .post('/b/devices/catalogs') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ modelCode: uniqueSeedValue('catalog').toUpperCase(), manufacturer: 'Global Vendor', name: '全局可调压阀', isValve: true, pressureLevels: ['10.0', '20', '30.0'], notes: '测试全局目录', }); expectSuccessEnvelope(createResponse, 201); expect(createResponse.body.data.isValve).toBe(true); expect(createResponse.body.data.pressureLevels).toEqual([ '10', '20', '30', ]); const updateResponse = await request(ctx.app.getHttpServer()) .patch(`/b/devices/catalogs/${createResponse.body.data.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ name: '全局可调压阀-更新版', pressureLevels: ['0.5', '1.0', '1.50'], }); expectSuccessEnvelope(updateResponse, 200); expect(updateResponse.body.data.name).toBe('全局可调压阀-更新版'); expect(updateResponse.body.data.isValve).toBe(true); expect(updateResponse.body.data.pressureLevels).toEqual([ '0.5', '1', '1.5', ]); const deleteResponse = await request(ctx.app.getHttpServer()) .delete(`/b/devices/catalogs/${createResponse.body.data.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectSuccessEnvelope(deleteResponse, 200); expect(deleteResponse.body.data.id).toBe(createResponse.body.data.id); }); it('成功:SYSTEM_ADMIN 可新增非阀门目录,且不会保存压力挡位', async () => { const response = await request(ctx.app.getHttpServer()) .post('/b/devices/catalogs') .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ modelCode: uniqueSeedValue('tube').toUpperCase(), manufacturer: 'Global Vendor', name: '腹腔管', isValve: false, pressureLevels: ['1', '2'], }); expectSuccessEnvelope(response, 201); expect(response.body.data.isValve).toBe(false); expect(response.body.data.isPressureAdjustable).toBe(false); expect(response.body.data.pressureLevels).toEqual([]); }); it('角色矩阵:仅 SYSTEM_ADMIN 可维护全局植入物目录', async () => { await assertRoleMatrix({ name: 'POST /b/devices/catalogs role matrix', tokens: ctx.tokens, expectedStatusByRole: { [Role.SYSTEM_ADMIN]: 201, [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/devices/catalogs') .set('Authorization', `Bearer ${token}`) .send({ modelCode: uniqueSeedValue('catalog-role').toUpperCase(), manufacturer: 'Role Matrix Vendor', name: '角色矩阵目录', isValve: true, pressureLevels: ['10', '20'], }), sendWithoutToken: async () => request(ctx.app.getHttpServer()) .post('/b/devices/catalogs') .send({ modelCode: uniqueSeedValue('catalog-anon').toUpperCase(), manufacturer: 'Anon Vendor', name: '匿名目录', }), }); }); }); describe('设备 CRUD 流程', () => { it('成功:HOSPITAL_ADMIN 可创建设备', async () => { const created = await createDevice( ctx.tokens[Role.HOSPITAL_ADMIN], ctx.fixtures.patients.patientA1Id, ); expect(created.status).toBe(DeviceStatus.ACTIVE); expect(created.currentPressure).toBe('0'); expect(created.patient.id).toBe(ctx.fixtures.patients.patientA1Id); expect(created).not.toHaveProperty('snCode'); }); it('失败:HOSPITAL_ADMIN 绑定跨院患者返回 403', async () => { const response = await request(ctx.app.getHttpServer()) .post('/b/devices') .set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`) .send({ status: DeviceStatus.ACTIVE, patientId: ctx.fixtures.patients.patientB1Id, }); expectErrorEnvelope(response, 403, '仅可绑定当前权限范围内患者'); }); it('成功:SYSTEM_ADMIN 可更新设备状态与归属患者', async () => { const created = await createDevice( ctx.tokens[Role.SYSTEM_ADMIN], ctx.fixtures.patients.patientA1Id, ); const response = await request(ctx.app.getHttpServer()) .patch(`/b/devices/${created.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ status: DeviceStatus.INACTIVE, patientId: ctx.fixtures.patients.patientA2Id, }); expectSuccessEnvelope(response, 200); expect(response.body.data.status).toBe(DeviceStatus.INACTIVE); expect(response.body.data.patient.id).toBe( ctx.fixtures.patients.patientA2Id, ); expect(response.body.data.currentPressure).toBe('0'); }); it('失败:设备实例接口不允许手工更新 currentPressure', async () => { const created = await createDevice( ctx.tokens[Role.SYSTEM_ADMIN], ctx.fixtures.patients.patientA1Id, ); const response = await request(ctx.app.getHttpServer()) .patch(`/b/devices/${created.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`) .send({ currentPressure: '1.5', }); expectErrorEnvelope(response, 400, 'currentPressure should not exist'); }); it('成功:SYSTEM_ADMIN 可删除未被任务引用的设备', async () => { const created = await createDevice( ctx.tokens[Role.SYSTEM_ADMIN], ctx.fixtures.patients.patientA1Id, ); const response = await request(ctx.app.getHttpServer()) .delete(`/b/devices/${created.id}`) .set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`); expectSuccessEnvelope(response, 200); expect(response.body.data.id).toBe(created.id); }); }); });