tyt-api-nest/test/e2e/specs/devices.e2e-spec.ts
EL 0b5640a977 调压任务流程从“发布即指派”改为“发布待接收(PENDING) -> 工程师接收(ACCEPTED) -> 完成(COMPLETED)”。
新增工程师“取消接收”能力,任务可从 ACCEPTED 回退到 PENDING。
发布任务不再要求 engineerId,并增加同设备存在未结束任务时的重复发布拦截。
完成任务新增 completionMaterials 必填校验,仅允许图片/视频凭证,并在完成时落库。
植入物目录新增 isValve,区分阀门与管子;非阀门不维护压力挡位,阀门至少 1 个挡位。
患者设备与任务查询返回新增字段,前端任务页支持接收/取消接收/上传凭证后完成。
增补 Prisma 迁移、接口文档、E2E 用例与夹具修复逻辑。
2026-03-20 06:03:09 +08:00

287 lines
9.7 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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