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:
yuzhiran
2026-06-11 10:27:35 +08:00
parent 9276ab9028
commit e6b79ddb21
39 changed files with 4576 additions and 246 deletions
@@ -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)
})
})
})