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 + idCardHash 查询跨院生命周期', async () => { const response = await request(ctx.app.getHttpServer()) .get('/c/patients/lifecycle') .query({ phone: '13800002001', idCardHash: 'seed-id-card-cross-hospital', }); expectSuccessEnvelope(response, 200); expect(response.body.data.phone).toBe('13800002001'); expect(response.body.data.idCardHash).toBe('seed-id-card-cross-hospital'); 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', }); expectErrorEnvelope(response, 400, 'idCardHash 必须是字符串'); }); it('失败:不存在患者返回 404', async () => { const response = await request(ctx.app.getHttpServer()) .get('/c/patients/lifecycle') .query({ phone: '13800009999', idCardHash: 'not-exists-idcard-hash', }); expectErrorEnvelope(response, 404, '未找到匹配的患者档案'); }); }); });