鉴权改为登录态回库校验,新增 tokenValidAfter 失效时间,支持密码变更与 seed 重置后旧 token 立即失效 患者字段由 idCardHash 统一迁移为 idCard,新增身份证标准化逻辑并同步 C 端生命周期查询参数 组织模块增加小组删除限制(有成员时返回 409)并补充中文错误消息 任务取消接口支持可选 reason 字段(先透传事件层) 补齐 Prisma 迁移、文档说明和 E2E 用例(含设备模块与 token 失效场景)
189 lines
6.7 KiB
TypeScript
189 lines
6.7 KiB
TypeScript
import request from 'supertest';
|
||
import { 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,
|
||
} from '../helpers/e2e-http.helper.js';
|
||
|
||
describe('Patients Controllers (e2e)', () => {
|
||
let ctx: E2EContext;
|
||
|
||
beforeAll(async () => {
|
||
ctx = await createE2EContext();
|
||
});
|
||
|
||
afterAll(async () => {
|
||
await closeE2EContext(ctx);
|
||
});
|
||
|
||
describe('GET /b/patients', () => {
|
||
it('成功:按角色返回正确可见性范围', async () => {
|
||
const systemAdminResponse = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.query({ hospitalId: ctx.fixtures.hospitalAId })
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||
expectSuccessEnvelope(systemAdminResponse, 200);
|
||
const systemPatientIds = (
|
||
systemAdminResponse.body.data as Array<{ id: number }>
|
||
).map((item) => item.id);
|
||
expect(systemPatientIds).toEqual(
|
||
expect.arrayContaining([
|
||
ctx.fixtures.patients.patientA1Id,
|
||
ctx.fixtures.patients.patientA2Id,
|
||
ctx.fixtures.patients.patientA3Id,
|
||
]),
|
||
);
|
||
|
||
const hospitalAdminResponse = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.HOSPITAL_ADMIN]}`);
|
||
expectSuccessEnvelope(hospitalAdminResponse, 200);
|
||
const hospitalPatientIds = (
|
||
hospitalAdminResponse.body.data as Array<{ id: number }>
|
||
).map((item) => item.id);
|
||
expect(hospitalPatientIds).toEqual(
|
||
expect.arrayContaining([
|
||
ctx.fixtures.patients.patientA1Id,
|
||
ctx.fixtures.patients.patientA2Id,
|
||
ctx.fixtures.patients.patientA3Id,
|
||
]),
|
||
);
|
||
|
||
const directorResponse = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.DIRECTOR]}`);
|
||
expectSuccessEnvelope(directorResponse, 200);
|
||
const directorPatientIds = (
|
||
directorResponse.body.data as Array<{ id: number }>
|
||
).map((item) => item.id);
|
||
expect(directorPatientIds).toEqual(
|
||
expect.arrayContaining([
|
||
ctx.fixtures.patients.patientA1Id,
|
||
ctx.fixtures.patients.patientA2Id,
|
||
]),
|
||
);
|
||
expect(directorPatientIds).not.toContain(
|
||
ctx.fixtures.patients.patientA3Id,
|
||
);
|
||
|
||
const leaderResponse = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.LEADER]}`);
|
||
expectSuccessEnvelope(leaderResponse, 200);
|
||
const leaderPatientIds = (
|
||
leaderResponse.body.data as Array<{ id: number }>
|
||
).map((item) => item.id);
|
||
expect(leaderPatientIds).toEqual(
|
||
expect.arrayContaining([
|
||
ctx.fixtures.patients.patientA1Id,
|
||
ctx.fixtures.patients.patientA2Id,
|
||
]),
|
||
);
|
||
expect(leaderPatientIds).not.toContain(ctx.fixtures.patients.patientA3Id);
|
||
|
||
const doctorResponse = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
|
||
expectSuccessEnvelope(doctorResponse, 200);
|
||
const doctorPatientIds = (
|
||
doctorResponse.body.data as Array<{ id: number }>
|
||
).map((item) => item.id);
|
||
expect(doctorPatientIds).toContain(ctx.fixtures.patients.patientA1Id);
|
||
expect(doctorPatientIds).not.toContain(ctx.fixtures.patients.patientA2Id);
|
||
expect(doctorPatientIds).not.toContain(ctx.fixtures.patients.patientA3Id);
|
||
|
||
const engineerResponse = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.ENGINEER]}`);
|
||
expectErrorEnvelope(engineerResponse, 403, '无权限执行当前操作');
|
||
});
|
||
|
||
it('失败:SYSTEM_ADMIN 不传 hospitalId 返回 400', async () => {
|
||
const response = await request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`);
|
||
|
||
expectErrorEnvelope(
|
||
response,
|
||
400,
|
||
'系统管理员查询必须显式传入 hospitalId',
|
||
);
|
||
});
|
||
|
||
it('角色矩阵:SYSTEM_ADMIN/HOSPITAL_ADMIN/DIRECTOR/LEADER/DOCTOR 可访问,ENGINEER 403,未登录 401', async () => {
|
||
await assertRoleMatrix({
|
||
name: 'GET /b/patients role matrix',
|
||
tokens: ctx.tokens,
|
||
expectedStatusByRole: {
|
||
[Role.SYSTEM_ADMIN]: 200,
|
||
[Role.HOSPITAL_ADMIN]: 200,
|
||
[Role.DIRECTOR]: 200,
|
||
[Role.LEADER]: 200,
|
||
[Role.DOCTOR]: 200,
|
||
[Role.ENGINEER]: 403,
|
||
},
|
||
sendAsRole: async (role, token) => {
|
||
const req = request(ctx.app.getHttpServer())
|
||
.get('/b/patients')
|
||
.set('Authorization', `Bearer ${token}`);
|
||
|
||
if (role === Role.SYSTEM_ADMIN) {
|
||
req.query({ hospitalId: ctx.fixtures.hospitalAId });
|
||
}
|
||
|
||
return req;
|
||
},
|
||
sendWithoutToken: async () =>
|
||
request(ctx.app.getHttpServer()).get('/b/patients'),
|
||
});
|
||
});
|
||
});
|
||
|
||
describe('GET /c/patients/lifecycle', () => {
|
||
it('成功:已登录用户可按 phone + idCard 查询跨院生命周期', async () => {
|
||
const response = await request(ctx.app.getHttpServer())
|
||
.get('/c/patients/lifecycle')
|
||
.query({
|
||
phone: '13800002001',
|
||
idCard: '110101199001010011',
|
||
})
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
|
||
|
||
expectSuccessEnvelope(response, 200);
|
||
expect(response.body.data.phone).toBe('13800002001');
|
||
expect(response.body.data.idCard).toBe('110101199001010011');
|
||
expect(response.body.data.patientCount).toBeGreaterThanOrEqual(2);
|
||
expect(Array.isArray(response.body.data.lifecycle)).toBe(true);
|
||
});
|
||
|
||
it('失败:参数缺失返回 400', async () => {
|
||
const response = await request(ctx.app.getHttpServer())
|
||
.get('/c/patients/lifecycle')
|
||
.query({
|
||
phone: '13800002001',
|
||
})
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
|
||
|
||
expectErrorEnvelope(response, 400, 'idCard 必须是字符串');
|
||
});
|
||
|
||
it('失败:不存在患者返回 404', async () => {
|
||
const response = await request(ctx.app.getHttpServer())
|
||
.get('/c/patients/lifecycle')
|
||
.query({
|
||
phone: '13800009999',
|
||
idCard: '110101199009090099',
|
||
})
|
||
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
|
||
|
||
expectErrorEnvelope(response, 404, '未找到匹配的患者档案');
|
||
});
|
||
});
|
||
});
|