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:
@@ -0,0 +1,168 @@
|
||||
import { Test, TestingModule } from '@nestjs/testing'
|
||||
import { getModelToken } from '@nestjs/mongoose'
|
||||
import { PaymentController } from './payment.controller'
|
||||
import { WechatPayService } from './wechat-pay.service'
|
||||
|
||||
describe('PaymentController', () => {
|
||||
let controller: PaymentController
|
||||
let mockUserModel: any
|
||||
let mockOrderModel: any
|
||||
let mockWechatPay: any
|
||||
|
||||
const mockUserId = '507f1f77bcf86cd799439011'
|
||||
|
||||
beforeEach(async () => {
|
||||
mockUserModel = {
|
||||
findById: jest.fn(),
|
||||
findByIdAndUpdate: jest.fn(),
|
||||
create: jest.fn(),
|
||||
}
|
||||
mockOrderModel = {
|
||||
findOne: jest.fn(),
|
||||
create: jest.fn(),
|
||||
}
|
||||
mockWechatPay = {
|
||||
nativePay: jest.fn().mockResolvedValue({ codeUrl: 'weixin://pay/abc123' }),
|
||||
jsapiPay: jest.fn().mockResolvedValue({ paySign: 'mock-sign', nonceStr: 'mock-nonce', package: 'prepay_id=mock', timeStamp: '123456', signType: 'RSA' }),
|
||||
queryOrder: jest.fn().mockResolvedValue({ trade_state: 'SUCCESS', transaction_id: 'wx123' }),
|
||||
}
|
||||
|
||||
const module: TestingModule = await Test.createTestingModule({
|
||||
controllers: [PaymentController],
|
||||
providers: [
|
||||
{ provide: getModelToken('User'), useValue: mockUserModel },
|
||||
{ provide: getModelToken('PaymentOrder'), useValue: mockOrderModel },
|
||||
{ provide: WechatPayService, useValue: mockWechatPay },
|
||||
],
|
||||
}).compile()
|
||||
|
||||
controller = module.get<PaymentController>(PaymentController)
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
jest.clearAllMocks()
|
||||
})
|
||||
|
||||
describe('create', () => {
|
||||
it('should throw for invalid plan', async () => {
|
||||
await expect(controller.create(mockUserId, 'invalid'))
|
||||
.rejects.toThrow('无效套餐')
|
||||
})
|
||||
|
||||
it('should throw if user not found', async () => {
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
||||
await expect(controller.create(mockUserId, 'growth'))
|
||||
.rejects.toThrow('用户不存在')
|
||||
})
|
||||
|
||||
it('should throw if user already has plan', async () => {
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'growth' }) })
|
||||
await expect(controller.create(mockUserId, 'growth'))
|
||||
.rejects.toThrow('已是会员')
|
||||
})
|
||||
|
||||
it('should create a growth order', async () => {
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', phone: '13800138000' }) })
|
||||
mockOrderModel.create.mockResolvedValue({})
|
||||
|
||||
const result = await controller.create(mockUserId, 'growth')
|
||||
expect(result).toHaveProperty('codeUrl')
|
||||
expect(result).toHaveProperty('outTradeNo')
|
||||
expect(mockWechatPay.nativePay).toHaveBeenCalled()
|
||||
expect(mockOrderModel.create).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should create a sprint order', async () => {
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', phone: '13800138000' }) })
|
||||
mockOrderModel.create.mockResolvedValue({})
|
||||
|
||||
const result = await controller.create(mockUserId, 'sprint')
|
||||
expect(result).toHaveProperty('codeUrl')
|
||||
expect(mockWechatPay.nativePay).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('jsapi', () => {
|
||||
it('should throw if no wxOpenid', async () => {
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', wxOpenid: null }) })
|
||||
await expect(controller.jsapi(mockUserId, 'growth'))
|
||||
.rejects.toThrow('未绑定微信')
|
||||
})
|
||||
|
||||
it('should return JSAPI pay params', async () => {
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ plan: 'free', wxOpenid: 'mock-openid', phone: '13800138000' }) })
|
||||
mockOrderModel.create.mockResolvedValue({})
|
||||
|
||||
const result = await controller.jsapi(mockUserId, 'growth')
|
||||
expect(result).toHaveProperty('paySign')
|
||||
expect(result).toHaveProperty('outTradeNo')
|
||||
})
|
||||
})
|
||||
|
||||
describe('checkOrder', () => {
|
||||
it('should throw if order not found for user', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
||||
await expect(controller.checkOrder('no-such-order', mockUserId))
|
||||
.rejects.toThrow('订单不存在')
|
||||
})
|
||||
|
||||
it('should return order status', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ status: 'pending', plan: 'growth' }) })
|
||||
const result = await controller.checkOrder('ORD123', mockUserId)
|
||||
expect(result).toEqual({ status: 'pending', plan: 'growth' })
|
||||
expect(mockOrderModel.findOne).toHaveBeenCalledWith({ outTradeNo: 'ORD123', userId: mockUserId })
|
||||
})
|
||||
})
|
||||
|
||||
describe('activate', () => {
|
||||
it('should throw if order not found for user', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
||||
await expect(controller.activate(mockUserId, 'ORD123'))
|
||||
.rejects.toThrow('订单不存在')
|
||||
})
|
||||
|
||||
it('should throw if payment not completed', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ status: 'pending', plan: 'growth' }) })
|
||||
await expect(controller.activate(mockUserId, 'ORD123'))
|
||||
.rejects.toThrow('支付未完成')
|
||||
})
|
||||
|
||||
it('should activate growth plan', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId, status: 'success', plan: 'growth' }) })
|
||||
const mockUser = { plan: 'free', vipExpireAt: null, sprintExpireAt: null, sprintRemaining: 0, remaining: 0, save: jest.fn().mockResolvedValue(true) }
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) })
|
||||
|
||||
const result = await controller.activate(mockUserId, 'ORD123')
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.plan).toBe('growth')
|
||||
expect(mockUser.save).toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('should activate sprint plan', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId, status: 'success', plan: 'sprint' }) })
|
||||
const mockUser = { plan: 'free', vipExpireAt: null, sprintExpireAt: null, sprintRemaining: 0, remaining: 0, save: jest.fn().mockResolvedValue(true) }
|
||||
mockUserModel.findById.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(mockUser) })
|
||||
|
||||
const result = await controller.activate(mockUserId, 'ORD123')
|
||||
expect(result.success).toBe(true)
|
||||
expect(result.plan).toBe('sprint')
|
||||
expect(mockUser.sprintRemaining).toBe(10)
|
||||
expect(mockUser.save).toHaveBeenCalled()
|
||||
})
|
||||
})
|
||||
|
||||
describe('query', () => {
|
||||
it('should throw if order not owned by user', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue(null) })
|
||||
await expect(controller.query('ORD123', mockUserId))
|
||||
.rejects.toThrow('订单不存在')
|
||||
})
|
||||
|
||||
it('should query WeChat for order status', async () => {
|
||||
mockOrderModel.findOne.mockReturnValueOnce({ exec: jest.fn().mockResolvedValue({ outTradeNo: 'ORD123', userId: mockUserId }) })
|
||||
const result = await controller.query('ORD123', mockUserId)
|
||||
expect(result).toHaveProperty('trade_state', 'SUCCESS')
|
||||
expect(mockWechatPay.queryOrder).toHaveBeenCalledWith('ORD123')
|
||||
})
|
||||
})
|
||||
})
|
||||
@@ -120,7 +120,9 @@ export class PaymentController {
|
||||
/** 查询订单(微信侧) */
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@Post('query')
|
||||
async query(@Body('outTradeNo') outTradeNo: string) {
|
||||
async query(@Body('outTradeNo') outTradeNo: string, @CurrentUser('userId') userId: string) {
|
||||
const order = await this.orderModel.findOne({ outTradeNo, userId }).exec()
|
||||
if (!order) throw new HttpException('订单不存在', HttpStatus.NOT_FOUND)
|
||||
return this.wechatPay.queryOrder(outTradeNo)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user