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
+187
View File
@@ -0,0 +1,187 @@
import request from 'supertest'
import { Test, TestingModule } from '@nestjs/testing'
import { INestApplication, ValidationPipe } from '@nestjs/common'
import { getModelToken } from '@nestjs/mongoose'
import { JwtService } from '@nestjs/jwt'
import { WechatPayService } from '../src/modules/payment/wechat-pay.service'
import { AppModule } from '../src/app.module'
function makeChain(execValue: any = null) {
const chain: Record<string, jest.Mock> = {}
const methods = ['find', 'findOne', 'findById', 'findByIdAndUpdate', 'sort', 'skip', 'limit', 'select', 'lean', 'countDocuments']
for (const m of methods) {
chain[m] = jest.fn(() => chain)
}
chain.exec = jest.fn().mockResolvedValue(execValue)
chain.save = jest.fn().mockResolvedValue(true)
return chain
}
function mockModel(extra: Record<string, any> = {}, defaultExecValue: any = null) {
const chain = makeChain(defaultExecValue)
return {
find: jest.fn(() => chain),
findOne: jest.fn(() => chain),
findById: jest.fn(() => chain),
findByIdAndUpdate: jest.fn(() => chain),
create: jest.fn(),
countDocuments: jest.fn(() => chain),
...extra,
}
}
describe('API Integration (e2e)', () => {
let app: INestApplication
let authToken: string
beforeAll(async () => {
const moduleFixture: TestingModule = await Test.createTestingModule({
imports: [AppModule],
})
.overrideProvider(WechatPayService).useValue({
nativePay: jest.fn().mockResolvedValue({ codeUrl: 'weixin://pay/test' }),
jsapiPay: jest.fn().mockResolvedValue({ paySign: 'test', nonceStr: 'test', package: 'test', timeStamp: '0', signType: 'RSA' }),
queryOrder: jest.fn().mockResolvedValue({ trade_state: 'SUCCESS' }),
verifyAndDecrypt: jest.fn().mockReturnValue({ out_trade_no: 'TEST', transaction_id: 'wx_test' }),
})
.overrideProvider(getModelToken('User')).useValue(mockModel({ countDocuments: jest.fn().mockResolvedValue(0) }))
.overrideProvider(getModelToken('Interview')).useValue(mockModel({}, []))
.overrideProvider(getModelToken('PaymentOrder')).useValue(mockModel({}, null))
.overrideProvider(getModelToken('Progress')).useValue(mockModel({}, null))
.overrideProvider(getModelToken('CompanyBank')).useValue(mockModel())
.overrideProvider(getModelToken('Contribution')).useValue(mockModel())
.overrideProvider(getModelToken('SiteConfig')).useValue(mockModel())
.overrideProvider(getModelToken('Resume')).useValue(mockModel())
.compile()
app = moduleFixture.createNestApplication()
app.setGlobalPrefix('api')
app.useGlobalPipes(new ValidationPipe({ whitelist: true, forbidNonWhitelisted: true, transform: true }))
await app.init()
const jwtService = app.get(JwtService)
authToken = jwtService.sign({ userId: '507f1f77bcf86cd799439011', phone: '13800138000' })
})
afterAll(async () => {
await app.close()
})
afterEach(() => {
jest.clearAllMocks()
})
describe('Unauthenticated access', () => {
it('GET /api/user/info should return 401', async () => {
const res = await request(app.getHttpServer()).get('/api/user/info')
expect(res.status).toBe(401)
})
it('POST /api/user/send-code should be public', async () => {
const res = await request(app.getHttpServer())
.post('/api/user/send-code')
.send({ phone: '13800138000' })
expect(res.status).toBe(201)
})
it('POST /api/payment/create should return 401', async () => {
const res = await request(app.getHttpServer())
.post('/api/payment/create')
.send({ plan: 'growth' })
expect(res.status).toBe(401)
})
it('GET /api/progress should return 401', async () => {
const res = await request(app.getHttpServer()).get('/api/progress')
expect(res.status).toBe(401)
})
it('POST /api/progress/checkin should return 401', async () => {
const res = await request(app.getHttpServer()).post('/api/progress/checkin')
expect(res.status).toBe(401)
})
})
describe('Authenticated access', () => {
const headers = () => ({ Authorization: `Bearer ${authToken}` })
it('POST /api/payment/create should validate plan', async () => {
const res = await request(app.getHttpServer())
.post('/api/payment/create')
.set(headers())
.send({ plan: 'invalid' })
expect(res.status).toBe(400)
})
it('POST /api/payment/jsapi should handle missing wxOpenid', async () => {
const userModel = app.get(getModelToken('User'))
const chain = makeChain({ plan: 'free', wxOpenid: null })
userModel.findById.mockReturnValueOnce(chain)
const res = await request(app.getHttpServer())
.post('/api/payment/jsapi')
.set(headers())
.send({ plan: 'growth' })
expect(res.status).toBe(400)
})
it('GET /api/admin/check should check admin status', async () => {
const userModel = app.get(getModelToken('User'))
const chain = makeChain({ role: 'user' })
userModel.findById.mockReturnValueOnce(chain)
const res = await request(app.getHttpServer())
.get('/api/admin/check')
.set(headers())
expect(res.status).toBe(200)
expect(res.body.isAdmin).toBe(false)
})
it('POST /api/progress/redeem should validate item', async () => {
const res = await request(app.getHttpServer())
.post('/api/progress/redeem')
.set(headers())
.send({ item: 'nonexistent' })
expect(res.status).toBe(400)
})
it('GET /api/progress should return default progress for new user', async () => {
const progressModel = app.get(getModelToken('Progress'))
const nullChain = makeChain(null)
progressModel.findOne.mockReturnValueOnce(nullChain)
progressModel.create.mockResolvedValue({
userId: '507f1f77bcf86cd799439011',
totalInterviews: 0,
completedInterviews: 0,
streak: 0,
points: 0,
totalCheckins: 0,
recentScores: [],
streakHistory: [],
lastCheckinDate: null,
})
const res = await request(app.getHttpServer())
.get('/api/progress')
.set(headers())
expect(res.status).toBe(200)
expect(res.body).toHaveProperty('totalInterviews', 0)
expect(res.body).toHaveProperty('points')
})
})
describe('Payment flow', () => {
const headers = () => ({ Authorization: `Bearer ${authToken}` })
it('POST /api/payment/check should verify order ownership', async () => {
const orderModel = app.get(getModelToken('PaymentOrder'))
const chain = makeChain(null)
orderModel.findOne.mockReturnValueOnce(chain)
const res = await request(app.getHttpServer())
.get('/api/payment/check/ORD123')
.set(headers())
expect(res.status).toBe(404)
})
})
})