v4.3 安全修复+代码质量+测试体系+护城河验证
## 安全修复 (5项) - CRITICAL JWT 硬编码 fallback(jwt.strategy / app.module / user.module) - HIGH seed_admin.js MongoDB 凭据泄漏 - MEDIUM 邮箱验证码泄漏 - MEDIUM 支付订单查询 IDOR - MEDIUM 管理后台 NoSQL 注入 ## 代码质量 (14处) - console.log→Logger(user.service.ts) - as any 类型化(11处跨7个文件) - Schema 联合类型修复(progress.schema) - Module 依赖缺失修复(progress.module) ## 测试体系 (61项) - 后端单元测试 Jest(43项):BenchmarkService/UserService/PaymentController - 后端集成测试 Supertest(11项):API 认证/支付/进度/管理 - 前端单元测试 Vitest(7项):配置文件/API端点 - 浏览器自动化 Playwright(7项):API smoke test - 覆盖率报告 + e2e 配置 ## 护城河 P0-P5 启动验证通过 + 编译通过
This commit is contained in:
@@ -9,7 +9,7 @@ import { User, UserSchema } from './user.schema'
|
||||
imports: [
|
||||
MongooseModule.forFeature([{ name: User.name, schema: UserSchema }]),
|
||||
JwtModule.register({
|
||||
secret: process.env.JWT_SECRET || 'zhiyin-jwt-secret-2026',
|
||||
secret: process.env.JWT_SECRET,
|
||||
signOptions: { expiresIn: '7d' },
|
||||
}),
|
||||
],
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
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'
|
||||
|
||||
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 () => {
|
||||
mockUserModel = {
|
||||
findOne: jest.fn().mockReturnThis(),
|
||||
findById: jest.fn().mockReturnThis(),
|
||||
findByIdAndUpdate: jest.fn().mockReturnThis(),
|
||||
create: jest.fn().mockResolvedValue(mockUser),
|
||||
exec: jest.fn().mockResolvedValue(null),
|
||||
}
|
||||
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: JwtService, useValue: mockJwtService },
|
||||
{ provide: EmailService, useValue: mockEmailService },
|
||||
],
|
||||
}).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) })
|
||||
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) })
|
||||
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) })
|
||||
|
||||
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) })
|
||||
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)
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -1,5 +1,5 @@
|
||||
import * as bcrypt from 'bcrypt'
|
||||
import { Injectable, HttpException, HttpStatus } from '@nestjs/common'
|
||||
import { Injectable, HttpException, HttpStatus, Logger } from '@nestjs/common'
|
||||
import { InjectModel } from '@nestjs/mongoose'
|
||||
import { Model } from 'mongoose'
|
||||
import { JwtService } from '@nestjs/jwt'
|
||||
@@ -12,6 +12,8 @@ const emailCodeStore = new Map<string, { code: string; expiresAt: number }>()
|
||||
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private readonly logger = new Logger(UserService.name)
|
||||
|
||||
constructor(
|
||||
@InjectModel(User.name) private userModel: Model<UserDocument>,
|
||||
private jwtService: JwtService,
|
||||
@@ -26,7 +28,7 @@ export class UserService {
|
||||
codeStore.set(phone, { code, expiresAt: Date.now() + 5 * 60 * 1000 })
|
||||
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
console.log(`[DEV] Verification code for ${phone}: ${code}`)
|
||||
this.logger.log(`Verification code for ${phone}: ${code}`)
|
||||
}
|
||||
return { message: '验证码已发送' }
|
||||
}
|
||||
@@ -87,9 +89,11 @@ export class UserService {
|
||||
if (sent) {
|
||||
return { message: '验证码已发送到邮箱' }
|
||||
}
|
||||
// 邮件发送失败时返回 devCode 方便调试
|
||||
console.log(`[EMAIL] Dev code for ${email}: ${code}`)
|
||||
return { message: '验证码已发送(邮件服务暂不可用,开发模式)', devCode: code }
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
this.logger.log(`Email code for ${email}: ${code}`)
|
||||
return { message: '验证码已发送(邮件服务暂不可用,开发模式)', devCode: code }
|
||||
}
|
||||
return { message: '验证码已发送,请查收邮件' }
|
||||
}
|
||||
|
||||
async loginByEmail(email: string, code: string) {
|
||||
|
||||
Reference in New Issue
Block a user