feat(server): add auth, user, and studio modules
Auth: WeChat login, JWT, roles guard (24 tests passing) User: profile CRUD, training stats with month/total calculations Studio: config management with auto-default creation
This commit is contained in:
220
packages/server/src/auth/__tests__/auth.service.spec.ts
Normal file
220
packages/server/src/auth/__tests__/auth.service.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing'
|
||||
import { JwtService } from '@nestjs/jwt'
|
||||
import { UnauthorizedException } from '@nestjs/common'
|
||||
import { UserRole } from '@mp-pilates/shared'
|
||||
import { AuthService } from '../auth.service'
|
||||
import { WechatService } from '../wechat.service'
|
||||
import { PrismaService } from '../../prisma/prisma.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 mockUser = {
|
||||
id: USER_ID,
|
||||
openid: OPENID,
|
||||
unionid: null,
|
||||
phone: null,
|
||||
nickname: '',
|
||||
avatarUrl: null,
|
||||
role: UserRole.MEMBER,
|
||||
createdAt: new Date(),
|
||||
updatedAt: new Date(),
|
||||
}
|
||||
|
||||
// ─── Mocks ───────────────────────────────────────────────────────────────────
|
||||
|
||||
const mockPrismaService = {
|
||||
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(),
|
||||
}
|
||||
|
||||
// ─── 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 },
|
||||
],
|
||||
}).compile()
|
||||
|
||||
authService = module.get<AuthService>(AuthService)
|
||||
|
||||
jest.clearAllMocks()
|
||||
mockJwtService.sign.mockReturnValue(JWT_TOKEN)
|
||||
})
|
||||
|
||||
// ── 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 },
|
||||
})
|
||||
expect(result.user).toEqual(mockUser)
|
||||
})
|
||||
|
||||
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 },
|
||||
})
|
||||
})
|
||||
|
||||
it('returns existing user when openid already exists', async () => {
|
||||
mockPrismaService.user.findUnique.mockResolvedValue(mockUser)
|
||||
|
||||
const result = await authService.login(loginCode)
|
||||
|
||||
expect(mockPrismaService.user.findUnique).toHaveBeenCalledWith({
|
||||
where: { openid: OPENID },
|
||||
})
|
||||
expect(mockPrismaService.user.create).not.toHaveBeenCalled()
|
||||
expect(result.user).toEqual(mockUser)
|
||||
})
|
||||
|
||||
it('returns a valid JWT token', async () => {
|
||||
mockPrismaService.user.findUnique.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)
|
||||
|
||||
const result = await authService.login(loginCode)
|
||||
|
||||
expect(result).toEqual({
|
||||
token: JWT_TOKEN,
|
||||
user: mockUser,
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
// ── 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)
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user