/** * 支付工具函数单元测试 */ import { validatePaymentParams, validatePaymentSecurity, formatPaymentAmount, PaymentStateManager, PaymentStatus, retryPayment, PaymentRateLimiter, validateAmountConsistency, generatePaymentParamsHash, verifyPaymentParamsIntegrity } from '@/utils/payment' import { mockRequestPayment } from '~/__mocks__/taroMock' describe('Payment Utils', () => { beforeEach(() => { mockRequestPayment.mockClear() // 清除状态管理器实例 const stateManager = PaymentStateManager.getInstance() const allStates = stateManager.getAllPaymentStates() allStates.forEach((_, orderId) => { stateManager.clearPaymentState(orderId) }) }) describe('validatePaymentParams', () => { it('should validate correct payment parameters', () => { const validParams = { timeStamp: '1234567890', nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'abcdefghijklmnopqrstuvwxyz1234567890' } const result = validatePaymentParams(validParams) expect(result.valid).toBe(true) expect(result.errors).toHaveLength(0) }) it('should detect missing parameters', () => { const invalidParams = { timeStamp: '', nonceStr: '', package: '', signType: '', paySign: '' } const result = validatePaymentParams(invalidParams) expect(result.valid).toBe(false) expect(result.errors).toHaveLength(5) }) }) describe('validatePaymentSecurity', () => { it('should validate secure payment parameters', () => { const validParams = { timeStamp: Math.floor(Date.now() / 1000).toString(), nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'abcdefghijklmnopqrstuvwxyz1234567890' } const result = validatePaymentSecurity(123, 100, validParams) expect(result.valid).toBe(true) }) it('should detect expired timestamp', () => { const expiredParams = { timeStamp: '1000000000', // 很旧的时间戳 nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'abcdefghijklmnopqrstuvwxyz1234567890' } const result = validatePaymentSecurity(123, 100, expiredParams) expect(result.valid).toBe(false) expect(result.reason).toContain('支付参数已过期') }) it('should detect invalid sign type', () => { const invalidParams = { timeStamp: Math.floor(Date.now() / 1000).toString(), nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'INVALID', paySign: 'abcdefghijklmnopqrstuvwxyz1234567890' } const result = validatePaymentSecurity(123, 100, invalidParams) expect(result.valid).toBe(false) expect(result.reason).toContain('签名类型不支持') }) }) describe('formatPaymentAmount', () => { it('should convert yuan to fen correctly', () => { expect(formatPaymentAmount(100)).toBe(10000) expect(formatPaymentAmount(50.5)).toBe(5050) expect(formatPaymentAmount(0.01)).toBe(1) }) it('should round fractional amounts', () => { expect(formatPaymentAmount(100.499)).toBe(10050) // 100.499 * 100 = 10049.9,四舍五入为10050 expect(formatPaymentAmount(100.501)).toBe(10050) }) }) describe('PaymentStateManager', () => { it('should manage payment states correctly', () => { const stateManager = PaymentStateManager.getInstance() stateManager.setPaymentState(123, PaymentStatus.PROCESSING) expect(stateManager.getPaymentState(123)).toBe(PaymentStatus.PROCESSING) stateManager.setPaymentState(123, PaymentStatus.SUCCESS) expect(stateManager.getPaymentState(123)).toBe(PaymentStatus.SUCCESS) }) it('should detect duplicate payments', () => { const stateManager = PaymentStateManager.getInstance() stateManager.setPaymentState(123, PaymentStatus.PROCESSING) expect(stateManager.isDuplicatePayment(123)).toBe(true) stateManager.setPaymentState(123, PaymentStatus.SUCCESS) expect(stateManager.isDuplicatePayment(123)).toBe(true) stateManager.setPaymentState(123, PaymentStatus.FAILED) expect(stateManager.isDuplicatePayment(123)).toBe(false) }) it('should clear payment states', () => { const stateManager = PaymentStateManager.getInstance() stateManager.setPaymentState(123, PaymentStatus.PROCESSING) stateManager.clearPaymentState(123) expect(stateManager.getPaymentState(123)).toBeUndefined() }) }) describe('retryPayment', () => { it('should succeed on first attempt', async () => { // 直接模拟 requestWechatPayment 函数返回成功结果 const mockRequestWechatPayment = jest.fn() mockRequestWechatPayment.mockResolvedValueOnce({ success: true, type: 'success', result: {} }) const paymentFn = async () => { return await mockRequestWechatPayment() } const result = await retryPayment(paymentFn) expect(result.success).toBe(true) expect(result.type).toBe('success') expect(mockRequestWechatPayment).toHaveBeenCalledTimes(1) }) it('should retry on failure', async () => { // 直接模拟 requestWechatPayment 函数,第一次失败,第二次成功 const mockRequestWechatPayment = jest.fn() mockRequestWechatPayment .mockResolvedValueOnce({ success: false, type: 'error', message: '网络错误' }) .mockResolvedValueOnce({ success: true, type: 'success', result: {} }) const paymentFn = async () => { return await mockRequestWechatPayment() } const result = await retryPayment(paymentFn, 3, 10) expect(result.success).toBe(true) expect(result.type).toBe('success') expect(mockRequestWechatPayment).toHaveBeenCalledTimes(2) }) it('should not retry on user cancellation', async () => { // 直接模拟 requestWechatPayment 函数返回用户取消结果 const mockRequestWechatPayment = jest.fn() mockRequestWechatPayment.mockResolvedValueOnce({ success: false, type: 'cancel', message: '用户取消支付' }) const paymentFn = async () => { return await mockRequestWechatPayment() } const result = await retryPayment(paymentFn) expect(result.success).toBe(false) expect(result.type).toBe('cancel') expect(result.message).toBe('用户取消支付') expect(mockRequestWechatPayment).toHaveBeenCalledTimes(1) }) }) describe('PaymentRateLimiter', () => { it('should allow payments within rate limit', () => { const rateLimiter = PaymentRateLimiter.getInstance() for (let i = 0; i < 4; i++) { rateLimiter.recordAttempt(123) } const result = rateLimiter.isRateLimited(123) expect(result.limited).toBe(false) }) it('should block payments exceeding rate limit', () => { const rateLimiter = PaymentRateLimiter.getInstance() for (let i = 0; i < 5; i++) { rateLimiter.recordAttempt(123) } const result = rateLimiter.isRateLimited(123) expect(result.limited).toBe(true) expect(result.remainingTime).toBeGreaterThan(0) }) it('should clear attempts', () => { const rateLimiter = PaymentRateLimiter.getInstance() rateLimiter.recordAttempt(123) rateLimiter.clearAttempts(123) const result = rateLimiter.isRateLimited(123) expect(result.limited).toBe(false) }) }) describe('validateAmountConsistency', () => { it('should validate consistent amounts', () => { const result = validateAmountConsistency(100, 10000) expect(result.valid).toBe(true) }) it('should detect inconsistent amounts', () => { const result = validateAmountConsistency(100, 9999) expect(result.valid).toBe(false) expect(result.reason).toContain('金额不一致') }) }) describe('generatePaymentParamsHash', () => { it('should generate consistent hash for same parameters', () => { const params = { timeStamp: '1234567890', nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'signature' } const hash1 = generatePaymentParamsHash(params) const hash2 = generatePaymentParamsHash(params) expect(hash1).toBe(hash2) }) it('should generate different hash for different parameters', () => { const params1 = { timeStamp: '1234567890', nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'signature' } const params2 = { timeStamp: '1234567891', // 不同的时间戳 nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'signature' } const hash1 = generatePaymentParamsHash(params1) const hash2 = generatePaymentParamsHash(params2) expect(hash1).not.toBe(hash2) }) }) describe('verifyPaymentParamsIntegrity', () => { it('should verify identical parameters', () => { const originalParams = { timeStamp: '1234567890', nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'signature' } const receivedParams = { ...originalParams } const result = verifyPaymentParamsIntegrity(originalParams, receivedParams) expect(result.valid).toBe(true) }) it('should detect tampered parameters', () => { const originalParams = { timeStamp: '1234567890', nonceStr: 'abcdefghijklmnopqrstuvwxyz', package: 'prepay_id=wx1234567890', signType: 'RSA', paySign: 'signature' } const tamperedParams = { ...originalParams, package: 'prepay_id=wx9876543210' // 被篡改的预支付ID } const result = verifyPaymentParamsIntegrity(originalParams, tamperedParams) expect(result.valid).toBe(false) expect(result.reason).toContain('支付参数被篡改') }) }) })