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) }) 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) }) }) })