Files
mp-pilates/packages/server/src/auth/__tests__/auth.service.spec.ts
richarjiang 139882d7a1 feat: 支持课后评价与成长档案
完成后即可评价并订阅提醒,馆主可记录体测、笔记与成长照片,首页展示匿名星级均分。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-09-09 15:55:48 +08:00

313 lines
11 KiB
TypeScript

import { Test, TestingModule } from '@nestjs/testing'
import { JwtService } from '@nestjs/jwt'
import { UnauthorizedException } from '@nestjs/common'
import { MembershipStatus, UserRole } from '@mp-pilates/shared'
import { ConfigService } from '@nestjs/config'
import { AuthService, RANDOM_FN_TOKEN } from '../auth.service'
import { WechatService } from '../wechat.service'
import { PrismaService } from '../../prisma/prisma.service'
import { InviteService } from '../../invite/invite.service'
// ─── Fixtures ────────────────────────────────────────────────────────────────
const OPENID = 'test_openid_123'
const SESSION_KEY = 'test_session_key'
const USER_ID = 'user-uuid-001'
const JWT_TOKEN = 'signed.jwt.token'
const TEST_NICKNAME = '优雅普拉提'
const mockUser = {
id: USER_ID,
openid: OPENID,
unionid: null,
phone: null,
nickname: TEST_NICKNAME,
avatarUrl: null,
role: UserRole.MEMBER,
adminBookingSubscriptionCount: 0,
lastLoginAt: new Date('2024-01-01T00:00:00Z'),
createdAt: new Date(),
updatedAt: new Date(),
}
// ─── Mocks ───────────────────────────────────────────────────────────────────
const mockPrismaService = {
membership: {
count: jest.fn(),
},
user: {
findUnique: jest.fn(),
findUniqueOrThrow: jest.fn(),
create: jest.fn(),
update: jest.fn(),
},
}
const mockWechatService = {
code2Session: jest.fn(),
decryptData: jest.fn(),
}
const mockJwtService = {
sign: jest.fn(),
}
const mockInviteService = {
bindInviterToUser: jest.fn(),
}
const mockConfigService = {
get: jest.fn(),
}
// ─── Tests ───────────────────────────────────────────────────────────────────
describe('AuthService', () => {
let authService: AuthService
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
AuthService,
{ provide: PrismaService, useValue: mockPrismaService },
{ provide: WechatService, useValue: mockWechatService },
{ provide: JwtService, useValue: mockJwtService },
{ provide: InviteService, useValue: mockInviteService },
{ provide: ConfigService, useValue: mockConfigService },
{ provide: RANDOM_FN_TOKEN, useValue: () => 0 }, // deterministic nickname
],
}).compile()
authService = module.get<AuthService>(AuthService)
jest.clearAllMocks()
mockJwtService.sign.mockReturnValue(JWT_TOKEN)
mockPrismaService.membership.count.mockResolvedValue(0)
mockConfigService.get.mockImplementation((key: string) => key === 'WX_SUBSCRIBE_TEMPLATE_BOOKING_CONFIRMED' ? 'tmpl-booking-confirmed' : '')
})
// ── login ──────────────────────────────────────────────────────────────────
describe('login', () => {
const loginCode = 'wx_login_code_abc'
beforeEach(() => {
mockWechatService.code2Session.mockResolvedValue({
openid: OPENID,
sessionKey: SESSION_KEY,
})
})
it('creates a new user when openid is not found', async () => {
mockPrismaService.user.findUnique.mockResolvedValue(null)
mockPrismaService.user.create.mockResolvedValue(mockUser)
const result = await authService.login(loginCode)
expect(mockWechatService.code2Session).toHaveBeenCalledWith(loginCode)
expect(mockPrismaService.user.findUnique).toHaveBeenCalledWith({
where: { openid: OPENID },
})
expect(mockPrismaService.user.create).toHaveBeenCalledWith({
data: {
openid: OPENID,
nickname: TEST_NICKNAME,
adminBookingSubscriptionCount: 0,
lastLoginAt: expect.any(Date),
},
})
expect(result.user).toEqual(expect.objectContaining({
id: mockUser.id,
phone: mockUser.phone,
nickname: mockUser.nickname,
avatarUrl: mockUser.avatarUrl,
role: mockUser.role,
activeMembershipCount: 0,
inviteShareEligible: false,
adminBookingSubscriptionCount: 0,
}))
expect(result.user.subscriptionMessageTemplates.templates).toHaveLength(2)
expect(result.isNewUser).toBe(true)
expect(mockInviteService.bindInviterToUser).toHaveBeenCalledWith(USER_ID, undefined)
})
it('binds inviter for new users when inviterId is present', async () => {
mockPrismaService.user.findUnique.mockResolvedValue(null)
mockPrismaService.user.create.mockResolvedValue(mockUser)
await authService.login(loginCode, undefined, undefined, 'inviter-001')
expect(mockInviteService.bindInviterToUser).toHaveBeenCalledWith(USER_ID, 'inviter-001')
})
it('creates user with unionid when present', async () => {
const unionid = 'wx_union_id_xyz'
mockWechatService.code2Session.mockResolvedValue({
openid: OPENID,
sessionKey: SESSION_KEY,
unionid,
})
mockPrismaService.user.findUnique.mockResolvedValue(null)
mockPrismaService.user.create.mockResolvedValue({ ...mockUser, unionid })
await authService.login(loginCode)
expect(mockPrismaService.user.create).toHaveBeenCalledWith({
data: {
openid: OPENID,
unionid,
nickname: TEST_NICKNAME,
adminBookingSubscriptionCount: 0,
lastLoginAt: expect.any(Date),
},
})
})
it('returns existing user when openid already exists', async () => {
mockPrismaService.user.findUnique.mockResolvedValue(mockUser)
mockPrismaService.user.update.mockResolvedValue(mockUser)
const result = await authService.login(loginCode)
expect(mockPrismaService.user.findUnique).toHaveBeenCalledWith({
where: { openid: OPENID },
})
expect(mockPrismaService.user.create).not.toHaveBeenCalled()
expect(mockPrismaService.user.update).toHaveBeenCalledWith({
where: { id: USER_ID },
data: { lastLoginAt: expect.any(Date) },
})
expect(result.user).toEqual(expect.objectContaining({
id: mockUser.id,
nickname: mockUser.nickname,
role: mockUser.role,
}))
expect(result.isNewUser).toBe(false)
})
it('returns a valid JWT token', async () => {
mockPrismaService.user.findUnique.mockResolvedValue(mockUser)
mockPrismaService.user.update.mockResolvedValue(mockUser)
const result = await authService.login(loginCode)
expect(mockJwtService.sign).toHaveBeenCalledWith({
sub: USER_ID,
role: UserRole.MEMBER,
})
expect(result.token).toBe(JWT_TOKEN)
})
it('returns both token and user in result', async () => {
mockPrismaService.user.findUnique.mockResolvedValue(mockUser)
mockPrismaService.user.update.mockResolvedValue(mockUser)
const result = await authService.login(loginCode)
expect(result).toEqual(expect.objectContaining({
token: JWT_TOKEN,
isNewUser: false,
}))
expect(result.user).toEqual(expect.objectContaining({
id: mockUser.id,
subscriptionMessageTemplates: {
templates: [
expect.objectContaining({ scene: 'BOOKING_CREATED' }),
expect.objectContaining({ scene: 'ADMIN_BOOKING_CREATED' }),
],
},
}))
})
it('includes active membership count and invite eligibility in login response', async () => {
mockPrismaService.user.findUnique.mockResolvedValue(mockUser)
mockPrismaService.user.update.mockResolvedValue(mockUser)
mockPrismaService.membership.count.mockResolvedValue(2)
const result = await authService.login(loginCode)
expect(mockPrismaService.membership.count).toHaveBeenCalledWith({
where: {
userId: USER_ID,
status: MembershipStatus.ACTIVE,
},
})
expect(result.user.activeMembershipCount).toBe(2)
expect(result.user.inviteShareEligible).toBe(true)
})
})
// ── bindPhone ──────────────────────────────────────────────────────────────
describe('bindPhone', () => {
const encryptedData = 'encrypted_phone_data'
const iv = 'init_vector'
const phoneNumber = '+8613800138000'
beforeEach(async () => {
// Seed the in-memory session key store by running login first
mockWechatService.code2Session.mockResolvedValue({
openid: OPENID,
sessionKey: SESSION_KEY,
})
mockPrismaService.user.findUnique.mockResolvedValue(mockUser)
mockPrismaService.user.update.mockResolvedValue(mockUser)
mockJwtService.sign.mockReturnValue(JWT_TOKEN)
await authService.login('login_code')
})
it('updates phone number and returns the updated user', async () => {
const updatedUser = { ...mockUser, phone: phoneNumber }
mockWechatService.decryptData.mockReturnValue({
phoneNumber,
purePhoneNumber: '13800138000',
countryCode: '86',
})
mockPrismaService.user.update.mockResolvedValue(updatedUser)
const result = await authService.bindPhone(USER_ID, encryptedData, iv)
expect(mockWechatService.decryptData).toHaveBeenCalledWith(
SESSION_KEY,
encryptedData,
iv,
)
expect(mockPrismaService.user.update).toHaveBeenCalledWith({
where: { id: USER_ID },
data: { phone: phoneNumber },
})
expect(result).toEqual(updatedUser)
})
it('throws UnauthorizedException when session key is not found', async () => {
const unknownUserId = 'unknown-user-id'
await expect(
authService.bindPhone(unknownUserId, encryptedData, iv),
).rejects.toThrow(UnauthorizedException)
})
it('does not mutate the original user object', async () => {
const originalUser = { ...mockUser }
const updatedUser = { ...mockUser, phone: phoneNumber }
mockWechatService.decryptData.mockReturnValue({
phoneNumber,
purePhoneNumber: '13800138000',
countryCode: '86',
})
mockPrismaService.user.update.mockResolvedValue(updatedUser)
const result = await authService.bindPhone(USER_ID, encryptedData, iv)
// Original mock user should be unchanged
expect(mockUser.phone).toBeNull()
// Result is a new object with the updated phone
expect(result.phone).toBe(phoneNumber)
expect(result).not.toBe(originalUser)
})
})
})