tyt-api-nest/test/e2e/specs/auth.e2e-spec.ts
EL ab17204739 C端 miniapp 登录增加手机号唯一命中患者规则,命中多份档案返回 409
C端 my-lifecycle 由跨院多患者聚合改为单患者返回,结构统一为 patient + lifecycle
生命周期事件移除事件内重复 patient 字段,减少冗余
B端患者生命周期接口同步采用 patient + lifecycle 结构
新增并接入生命周期 Swagger 响应模型,补齐接口文档
更新 auth/patients/frontend 集成文档说明
增加 e2e:多患者冲突、C端/B端新返回结构、权限失败场景
2026-04-02 04:07:40 +08:00

360 lines
12 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 { Role } from '../../../src/generated/prisma/enums.js';
import {
E2E_SEED_CREDENTIALS,
E2E_SEED_PASSWORD,
} from '../fixtures/e2e-roles.js';
import {
closeE2EContext,
createE2EContext,
type E2EContext,
} from '../helpers/e2e-context.helper.js';
import { assertRoleMatrix } from '../helpers/e2e-matrix.helper.js';
import {
expectErrorEnvelope,
expectSuccessEnvelope,
uniquePhone,
uniqueSeedValue,
} from '../helpers/e2e-http.helper.js';
import { buildMiniAppMockPayload } from '../helpers/e2e-miniapp-auth.helper.js';
describe('AuthController (e2e)', () => {
let ctx: E2EContext;
beforeAll(async () => {
ctx = await createE2EContext();
});
afterAll(async () => {
await closeE2EContext(ctx);
});
describe('POST /auth/system-admin', () => {
it('成功:创建系统管理员账号', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/system-admin')
.send({
name: uniqueSeedValue('Auth 系统管理员'),
phone: uniquePhone(),
password: 'Seed@1234',
openId: uniqueSeedValue('auth-system-admin-openid'),
systemAdminBootstrapKey: process.env.SYSTEM_ADMIN_BOOTSTRAP_KEY,
});
expectSuccessEnvelope(response, 201);
expect(response.body.data.role).toBe(Role.SYSTEM_ADMIN);
});
it('失败:参数不合法返回 400', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/system-admin')
.send({
name: 'bad-system-admin',
phone: '13800009999',
password: '123',
systemAdminBootstrapKey: process.env.SYSTEM_ADMIN_BOOTSTRAP_KEY,
});
expectErrorEnvelope(response, 400, '密码长度至少 8 位');
});
});
describe('POST /auth/login', () => {
it('成功:院内账号可使用手机号密码登录', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/login')
.send({
phone: E2E_SEED_CREDENTIALS[Role.DOCTOR].phone,
password: E2E_SEED_PASSWORD,
role: Role.DOCTOR,
hospitalId: ctx.fixtures.hospitalAId,
});
expectSuccessEnvelope(response, 201);
expect(response.body.data.accessToken).toEqual(expect.any(String));
expect(response.body.data.actor.role).toBe(Role.DOCTOR);
});
it('成功:同手机号命中多个账号时先返回候选,再确认登录', async () => {
const sharedPhone = uniquePhone();
const firstUser = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
.send({
name: uniqueSeedValue('Password 多账号医生'),
phone: sharedPhone,
password: 'Seed@1234',
role: Role.DOCTOR,
hospitalId: ctx.fixtures.hospitalAId,
departmentId: ctx.fixtures.departmentA1Id,
groupId: ctx.fixtures.groupA1Id,
});
expectSuccessEnvelope(firstUser, 201);
const secondUser = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
.send({
name: uniqueSeedValue('Password 多账号工程师'),
phone: sharedPhone,
password: 'Seed@1234',
role: Role.ENGINEER,
hospitalId: ctx.fixtures.hospitalAId,
});
expectSuccessEnvelope(secondUser, 201);
const firstStage = await request(ctx.app.getHttpServer())
.post('/auth/login')
.send({
phone: sharedPhone,
password: 'Seed@1234',
});
expectSuccessEnvelope(firstStage, 201);
expect(firstStage.body.data.needSelect).toBe(true);
expect(firstStage.body.data.accounts).toHaveLength(2);
const confirmResponse = await request(ctx.app.getHttpServer())
.post('/auth/login/confirm')
.send({
loginTicket: firstStage.body.data.loginTicket,
userId: firstUser.body.data.id,
});
expectSuccessEnvelope(confirmResponse, 201);
expect(confirmResponse.body.data.user.id).toBe(firstUser.body.data.id);
});
it('失败:密码错误返回 401', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/login')
.send({
phone: E2E_SEED_CREDENTIALS[Role.DOCTOR].phone,
password: 'Wrong@1234',
role: Role.DOCTOR,
hospitalId: ctx.fixtures.hospitalAId,
});
expectErrorEnvelope(response, 401, '手机号、密码或角色不匹配');
});
});
describe('POST /auth/miniapp/b/phone-login', () => {
it('成功:单院内账号手机号可直接登录', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login')
.send(buildMiniAppMockPayload('13800001004', 'seed-doctor-a-openid'));
expectSuccessEnvelope(response, 201);
expect(response.body.data.accessToken).toEqual(expect.any(String));
expect(response.body.data.actor.role).toBe(Role.DOCTOR);
});
it('成功:同手机号多账号时先返回候选,再确认登录', async () => {
const sharedPhone = uniquePhone();
const firstUser = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
.send({
name: uniqueSeedValue('MiniApp 多账号医生'),
phone: sharedPhone,
role: Role.DOCTOR,
hospitalId: ctx.fixtures.hospitalAId,
departmentId: ctx.fixtures.departmentA1Id,
groupId: ctx.fixtures.groupA1Id,
});
expectSuccessEnvelope(firstUser, 201);
const secondUser = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
.send({
name: uniqueSeedValue('MiniApp 多账号工程师'),
phone: sharedPhone,
role: Role.ENGINEER,
hospitalId: ctx.fixtures.hospitalAId,
});
expectSuccessEnvelope(secondUser, 201);
const firstStage = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login')
.send(
buildMiniAppMockPayload(sharedPhone, uniqueSeedValue('multi-openid')),
);
expectSuccessEnvelope(firstStage, 201);
expect(firstStage.body.data.needSelect).toBe(true);
expect(firstStage.body.data.accounts).toHaveLength(2);
const confirmResponse = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login/confirm')
.send({
loginTicket: firstStage.body.data.loginTicket,
userId: firstUser.body.data.id,
});
expectSuccessEnvelope(confirmResponse, 201);
expect(confirmResponse.body.data.user.id).toBe(firstUser.body.data.id);
});
it('成功:同一微信 openId 可切换绑定多个院内账号', async () => {
const sharedPhone = uniquePhone();
const sharedOpenId = uniqueSeedValue('shared-openid');
const firstUser = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
.send({
name: uniqueSeedValue('MiniApp 切换账号医生'),
phone: sharedPhone,
role: Role.DOCTOR,
hospitalId: ctx.fixtures.hospitalAId,
departmentId: ctx.fixtures.departmentA1Id,
groupId: ctx.fixtures.groupA1Id,
});
expectSuccessEnvelope(firstUser, 201);
const secondUser = await request(ctx.app.getHttpServer())
.post('/users')
.set('Authorization', `Bearer ${ctx.tokens[Role.SYSTEM_ADMIN]}`)
.send({
name: uniqueSeedValue('MiniApp 切换账号工程师'),
phone: sharedPhone,
role: Role.ENGINEER,
hospitalId: ctx.fixtures.hospitalAId,
});
expectSuccessEnvelope(secondUser, 201);
const firstStageForDoctor = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login')
.send(buildMiniAppMockPayload(sharedPhone, sharedOpenId));
expectSuccessEnvelope(firstStageForDoctor, 201);
const doctorConfirm = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login/confirm')
.send({
loginTicket: firstStageForDoctor.body.data.loginTicket,
userId: firstUser.body.data.id,
});
expectSuccessEnvelope(doctorConfirm, 201);
const firstStageForEngineer = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login')
.send(buildMiniAppMockPayload(sharedPhone, sharedOpenId));
expectSuccessEnvelope(firstStageForEngineer, 201);
const engineerConfirm = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login/confirm')
.send({
loginTicket: firstStageForEngineer.body.data.loginTicket,
userId: secondUser.body.data.id,
});
expectSuccessEnvelope(engineerConfirm, 201);
const users = await ctx.prisma.user.findMany({
where: {
id: {
in: [firstUser.body.data.id, secondUser.body.data.id],
},
},
select: {
id: true,
openId: true,
},
orderBy: { id: 'asc' },
});
expect(users).toEqual([
{ id: firstUser.body.data.id, openId: sharedOpenId },
{ id: secondUser.body.data.id, openId: sharedOpenId },
]);
});
it('失败:手机号未匹配院内账号返回 404', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/b/phone-login')
.send(
buildMiniAppMockPayload(
uniquePhone(),
uniqueSeedValue('no-user-openid'),
),
);
expectErrorEnvelope(response, 404, '手机号未匹配到院内账号');
});
});
describe('POST /auth/miniapp/c/phone-login', () => {
it('成功:手机号唯一关联患者时可创建 C 端账号并返回 token', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/c/phone-login')
.send(buildMiniAppMockPayload('13800002002', 'seed-family-a2-openid'));
expectSuccessEnvelope(response, 201);
expect(response.body.data.accessToken).toEqual(expect.any(String));
expect(response.body.data.familyAccount.phone).toBe('13800002002');
});
it('失败:手机号未关联患者档案返回 404', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/c/phone-login')
.send(
buildMiniAppMockPayload(
uniquePhone(),
uniqueSeedValue('family-openid'),
),
);
expectErrorEnvelope(response, 404, '当前手机号未关联患者档案');
});
it('失败:手机号关联多份患者档案返回 409', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/c/phone-login')
.send(buildMiniAppMockPayload('13800002001', 'seed-family-a1-openid'));
expectErrorEnvelope(
response,
409,
'当前手机号关联了多份患者档案,请联系管理员处理',
);
});
});
describe('GET /auth/me', () => {
it('成功:已登录用户可读取当前信息', async () => {
const response = await request(ctx.app.getHttpServer())
.get('/auth/me')
.set('Authorization', `Bearer ${ctx.tokens[Role.DOCTOR]}`);
expectSuccessEnvelope(response, 200);
expect(response.body.data.role).toBe(Role.DOCTOR);
});
it('失败:未登录返回 401', async () => {
const response = await request(ctx.app.getHttpServer()).get('/auth/me');
expectErrorEnvelope(response, 401, '缺少 Bearer Token');
});
it('角色矩阵6 角色都可访问,未登录 401', async () => {
await assertRoleMatrix({
name: 'GET /auth/me 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]: 200,
},
sendAsRole: async (_role, token) =>
request(ctx.app.getHttpServer())
.get('/auth/me')
.set('Authorization', `Bearer ${token}`),
sendWithoutToken: async () =>
request(ctx.app.getHttpServer()).get('/auth/me'),
});
});
});
});