2230a95b45
- New GravityTransaction schema tracks all gravity changes (registration, interview/optimize/download deduction, purchase, monthly topup, migration) - GravityTopUpService: bulk log for monthly VIP topup - PaymentController.activateMembership: log plan_set transactions - QuotaService: add logTransaction, wire into all gravity-modifying methods - UserService: log registration grants (phone/wx/email/password) - GET /user/gravity-transactions?page=&limit= API - Frontend: user.vue '明细' button + paginated popup - Docs: update PROJECT-STATUS v4.10, FEATURE-LIST, DEPLOYMENT
162 lines
6.3 KiB
TypeScript
162 lines
6.3 KiB
TypeScript
import { Test, TestingModule } from '@nestjs/testing'
|
|
import { getModelToken } from '@nestjs/mongoose'
|
|
import { JwtService } from '@nestjs/jwt'
|
|
import { HttpException } from '@nestjs/common'
|
|
import { UserService } from './user.service'
|
|
import { EmailService } from '../email/email.service'
|
|
import { PricingService } from '../schemas/pricing.service'
|
|
|
|
describe('UserService', () => {
|
|
let service: UserService
|
|
let mockUserModel: any
|
|
let mockJwtService: any
|
|
let mockEmailService: any
|
|
|
|
const mockUser = {
|
|
_id: '507f1f77bcf86cd799439011',
|
|
phone: '13800138000',
|
|
nickname: '测试用户',
|
|
email: 'test@example.com',
|
|
plan: 'free',
|
|
role: 'user',
|
|
isSystemAdmin: false,
|
|
remaining: 3,
|
|
interviewCount: 0,
|
|
password: null,
|
|
save: jest.fn().mockResolvedValue(true),
|
|
}
|
|
|
|
beforeEach(async () => {
|
|
const chainable = (value: any) => ({ exec: jest.fn().mockResolvedValue(value), select: jest.fn().mockReturnThis() })
|
|
mockUserModel = {
|
|
findOne: jest.fn().mockReturnValue(chainable(null)),
|
|
findById: jest.fn().mockReturnValue(chainable(null)),
|
|
findByIdAndUpdate: jest.fn().mockReturnValue(chainable(null)),
|
|
create: jest.fn().mockResolvedValue(mockUser),
|
|
}
|
|
mockJwtService = {
|
|
sign: jest.fn().mockReturnValue('mock-jwt-token'),
|
|
}
|
|
mockEmailService = {
|
|
sendVerificationCode: jest.fn().mockResolvedValue(true),
|
|
}
|
|
|
|
const module: TestingModule = await Test.createTestingModule({
|
|
providers: [
|
|
UserService,
|
|
{ provide: getModelToken('User'), useValue: mockUserModel },
|
|
{ provide: getModelToken('GravityTransaction'), useValue: { create: jest.fn() } },
|
|
{ provide: JwtService, useValue: mockJwtService },
|
|
{ provide: EmailService, useValue: mockEmailService },
|
|
{ provide: PricingService, useValue: { getConfig: jest.fn().mockResolvedValue({ registrationGravity: 50, gravityRates: { interviewPerUse: 5, optimizePerUse: 3, downloadPerUse: 2 }, plans: { growth: { gravityPerMonth: 80 }, sprint: { gravityPerMonth: 200 } } }) } },
|
|
],
|
|
}).compile()
|
|
|
|
service = module.get<UserService>(UserService)
|
|
})
|
|
|
|
afterEach(() => {
|
|
jest.clearAllMocks()
|
|
})
|
|
|
|
describe('sendCode', () => {
|
|
it('should return success message', async () => {
|
|
const result = await service.sendCode('13800138000')
|
|
expect(result).toEqual({ message: '验证码已发送' })
|
|
})
|
|
})
|
|
|
|
describe('loginByPhone', () => {
|
|
it('should throw on wrong code', async () => {
|
|
await expect(service.loginByPhone('13800138000', 'wrong'))
|
|
.rejects.toThrow(HttpException)
|
|
})
|
|
|
|
it('should create user on first login and return token', async () => {
|
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null), select: jest.fn().mockReturnThis() })
|
|
await service.sendCode('13800138000')
|
|
const result = await service.loginByPhone('13800138000', '123456')
|
|
expect(result).toHaveProperty('token', 'mock-jwt-token')
|
|
expect(result.user).toHaveProperty('id')
|
|
expect(mockUserModel.create).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should login existing user', async () => {
|
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser), select: jest.fn().mockReturnThis() })
|
|
await service.sendCode('13800138000')
|
|
const result = await service.loginByPhone('13800138000', '123456')
|
|
expect(result).toHaveProperty('token')
|
|
})
|
|
})
|
|
|
|
describe('sendEmailCode', () => {
|
|
it('should reject invalid email', async () => {
|
|
await expect(service.sendEmailCode('invalid'))
|
|
.rejects.toThrow(HttpException)
|
|
})
|
|
|
|
it('should send email verification code', async () => {
|
|
const result = await service.sendEmailCode('test@example.com')
|
|
expect(result).toEqual({ message: '验证码已发送到邮箱' })
|
|
})
|
|
})
|
|
|
|
describe('loginByEmail', () => {
|
|
it('should throw on wrong code', async () => {
|
|
await expect(service.loginByEmail('test@example.com', 'wrong'))
|
|
.rejects.toThrow(HttpException)
|
|
})
|
|
|
|
it('should login with valid email code', async () => {
|
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser), select: jest.fn().mockReturnThis() })
|
|
|
|
const spy = jest.spyOn(mockEmailService, 'sendVerificationCode')
|
|
await service.sendEmailCode('test@example.com')
|
|
const storedCode = spy.mock.calls[0][1] as string
|
|
|
|
const result = await service.loginByEmail('test@example.com', storedCode)
|
|
expect(result).toHaveProperty('token')
|
|
expect(result.isNew).toBe(false)
|
|
})
|
|
})
|
|
|
|
describe('loginByPassword', () => {
|
|
it('should throw for nonexistent user', async () => {
|
|
mockUserModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null), select: jest.fn().mockReturnThis() })
|
|
await expect(service.loginByPassword('test@example.com', 'pass'))
|
|
.rejects.toThrow(HttpException)
|
|
})
|
|
})
|
|
|
|
describe('getInfo', () => {
|
|
it('should return user info', async () => {
|
|
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) })
|
|
const result = await service.getInfo('507f1f77bcf86cd799439011')
|
|
expect(result).toHaveProperty('id', mockUser._id)
|
|
expect(result).toHaveProperty('phone', mockUser.phone)
|
|
})
|
|
|
|
it('should throw for nonexistent user', async () => {
|
|
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
|
await expect(service.getInfo('nonexistent')).rejects.toThrow(HttpException)
|
|
})
|
|
})
|
|
|
|
describe('deductRemaining', () => {
|
|
it('should decrement remaining count', async () => {
|
|
const user = { ...mockUser, remaining: 3, interviewCount: 0, save: jest.fn().mockResolvedValue(true) }
|
|
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(user) })
|
|
await service.deductRemaining('507f1f77bcf86cd799439011')
|
|
expect(user.remaining).toBe(2)
|
|
expect(user.interviewCount).toBe(1)
|
|
expect(user.save).toHaveBeenCalled()
|
|
})
|
|
|
|
it('should throw when no remaining', async () => {
|
|
const user = { ...mockUser, remaining: 0, save: jest.fn() }
|
|
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(user) })
|
|
await expect(service.deductRemaining('507f1f77bcf86cd799439011')).rejects.toThrow(HttpException)
|
|
})
|
|
})
|
|
})
|