tyt-api-nest/test/e2e/specs/auth.e2e-spec.ts

264 lines
9.3 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 { 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('失败:手机号未匹配院内账号返回 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('成功:手机号关联患者时可创建家属账号并返回 token', async () => {
const response = await request(ctx.app.getHttpServer())
.post('/auth/miniapp/c/phone-login')
.send(buildMiniAppMockPayload('13800002001', 'seed-family-a1-openid'));
expectSuccessEnvelope(response, 201);
expect(response.body.data.accessToken).toEqual(expect.any(String));
expect(response.body.data.familyAccount.phone).toBe('13800002001');
});
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, '当前手机号未关联患者档案');
});
});
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'),
});
});
});
});