|
|
@@ -0,0 +1,344 @@
|
|
|
+/**
|
|
|
+ * 支付工具函数单元测试
|
|
|
+ */
|
|
|
+
|
|
|
+import {
|
|
|
+ validatePaymentParams,
|
|
|
+ validatePaymentSecurity,
|
|
|
+ formatPaymentAmount,
|
|
|
+ checkPaymentEnvironment,
|
|
|
+ PaymentStateManager,
|
|
|
+ PaymentStatus,
|
|
|
+ retryPayment,
|
|
|
+ PaymentRateLimiter,
|
|
|
+ validateAmountConsistency,
|
|
|
+ generatePaymentParamsHash,
|
|
|
+ verifyPaymentParamsIntegrity
|
|
|
+} from '@/utils/payment'
|
|
|
+
|
|
|
+// Mock Taro.requestPayment
|
|
|
+const mockRequestPayment = jest.fn()
|
|
|
+jest.mock('@tarojs/taro', () => ({
|
|
|
+ default: {
|
|
|
+ requestPayment: mockRequestPayment
|
|
|
+ }
|
|
|
+}))
|
|
|
+
|
|
|
+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('支付参数被篡改')
|
|
|
+ })
|
|
|
+ })
|
|
|
+})
|