tyt-api-nest/test/e2e/specs/devices.e2e-spec.ts
EL 2bfe8ac8c8 新增上传资产模型与迁移,支持 IMAGE、VIDEO、FILE 三类资产管理
新增 B 端上传接口与列表接口,统一文件上传和分页查询能力
上传能力支持医院级数据隔离:系统管理员需显式指定医院,院内角色按登录医院自动隔离
图片上传自动压缩并转为 webp,视频上传自动转码并压缩为 mp4,普通文件按原始类型存储
增加上传目录与公开访问能力,统一输出可直接预览的访问地址
前端新增影像库页面,支持按类型筛选、关键字检索、分页浏览、在线预览与原文件访问
前端新增通用上传组件,支持在页面内复用并返回上传结果
管理后台新增影像库菜单与路由,并补充页面级角色权限控制
患者手术相关表单接入上传复用能力,支持术前资料与设备标签上传回填
新增上传模块 e2e 用例,覆盖成功路径、权限矩阵与关键失败场景
补充上传模块文档与安装依赖说明,完善工程内使用说明
2026-03-20 04:35:43 +08:00

263 lines
8.9 KiB
TypeScript
Raw 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: '全局可调压阀',
isPressureAdjustable: true,
pressureLevels: ['10.0', '20', '30.0'],
notes: '测试全局目录',
});
expectSuccessEnvelope(createResponse, 201);
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.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 () => {
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: '角色矩阵目录',
isPressureAdjustable: 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);
});
});
});