payment.test.ts 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338
  1. /**
  2. * 支付工具函数单元测试
  3. */
  4. import {
  5. validatePaymentParams,
  6. validatePaymentSecurity,
  7. formatPaymentAmount,
  8. PaymentStateManager,
  9. PaymentStatus,
  10. retryPayment,
  11. PaymentRateLimiter,
  12. validateAmountConsistency,
  13. generatePaymentParamsHash,
  14. verifyPaymentParamsIntegrity
  15. } from '@/utils/payment'
  16. import { mockRequestPayment } from '~/__mocks__/taroMock'
  17. describe('Payment Utils', () => {
  18. beforeEach(() => {
  19. mockRequestPayment.mockClear()
  20. // 清除状态管理器实例
  21. const stateManager = PaymentStateManager.getInstance()
  22. const allStates = stateManager.getAllPaymentStates()
  23. allStates.forEach((_, orderId) => {
  24. stateManager.clearPaymentState(orderId)
  25. })
  26. })
  27. describe('validatePaymentParams', () => {
  28. it('should validate correct payment parameters', () => {
  29. const validParams = {
  30. timeStamp: '1234567890',
  31. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  32. package: 'prepay_id=wx1234567890',
  33. signType: 'RSA',
  34. paySign: 'abcdefghijklmnopqrstuvwxyz1234567890'
  35. }
  36. const result = validatePaymentParams(validParams)
  37. expect(result.valid).toBe(true)
  38. expect(result.errors).toHaveLength(0)
  39. })
  40. it('should detect missing parameters', () => {
  41. const invalidParams = {
  42. timeStamp: '',
  43. nonceStr: '',
  44. package: '',
  45. signType: '',
  46. paySign: ''
  47. }
  48. const result = validatePaymentParams(invalidParams)
  49. expect(result.valid).toBe(false)
  50. expect(result.errors).toHaveLength(5)
  51. })
  52. })
  53. describe('validatePaymentSecurity', () => {
  54. it('should validate secure payment parameters', () => {
  55. const validParams = {
  56. timeStamp: Math.floor(Date.now() / 1000).toString(),
  57. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  58. package: 'prepay_id=wx1234567890',
  59. signType: 'RSA',
  60. paySign: 'abcdefghijklmnopqrstuvwxyz1234567890'
  61. }
  62. const result = validatePaymentSecurity(123, 100, validParams)
  63. expect(result.valid).toBe(true)
  64. })
  65. it('should detect expired timestamp', () => {
  66. const expiredParams = {
  67. timeStamp: '1000000000', // 很旧的时间戳
  68. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  69. package: 'prepay_id=wx1234567890',
  70. signType: 'RSA',
  71. paySign: 'abcdefghijklmnopqrstuvwxyz1234567890'
  72. }
  73. const result = validatePaymentSecurity(123, 100, expiredParams)
  74. expect(result.valid).toBe(false)
  75. expect(result.reason).toContain('支付参数已过期')
  76. })
  77. it('should detect invalid sign type', () => {
  78. const invalidParams = {
  79. timeStamp: Math.floor(Date.now() / 1000).toString(),
  80. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  81. package: 'prepay_id=wx1234567890',
  82. signType: 'INVALID',
  83. paySign: 'abcdefghijklmnopqrstuvwxyz1234567890'
  84. }
  85. const result = validatePaymentSecurity(123, 100, invalidParams)
  86. expect(result.valid).toBe(false)
  87. expect(result.reason).toContain('签名类型不支持')
  88. })
  89. })
  90. describe('formatPaymentAmount', () => {
  91. it('should convert yuan to fen correctly', () => {
  92. expect(formatPaymentAmount(100)).toBe(10000)
  93. expect(formatPaymentAmount(50.5)).toBe(5050)
  94. expect(formatPaymentAmount(0.01)).toBe(1)
  95. })
  96. it('should round fractional amounts', () => {
  97. expect(formatPaymentAmount(100.499)).toBe(10050) // 100.499 * 100 = 10049.9,四舍五入为10050
  98. expect(formatPaymentAmount(100.501)).toBe(10050)
  99. })
  100. })
  101. describe('PaymentStateManager', () => {
  102. it('should manage payment states correctly', () => {
  103. const stateManager = PaymentStateManager.getInstance()
  104. stateManager.setPaymentState(123, PaymentStatus.PROCESSING)
  105. expect(stateManager.getPaymentState(123)).toBe(PaymentStatus.PROCESSING)
  106. stateManager.setPaymentState(123, PaymentStatus.SUCCESS)
  107. expect(stateManager.getPaymentState(123)).toBe(PaymentStatus.SUCCESS)
  108. })
  109. it('should detect duplicate payments', () => {
  110. const stateManager = PaymentStateManager.getInstance()
  111. stateManager.setPaymentState(123, PaymentStatus.PROCESSING)
  112. expect(stateManager.isDuplicatePayment(123)).toBe(true)
  113. stateManager.setPaymentState(123, PaymentStatus.SUCCESS)
  114. expect(stateManager.isDuplicatePayment(123)).toBe(true)
  115. stateManager.setPaymentState(123, PaymentStatus.FAILED)
  116. expect(stateManager.isDuplicatePayment(123)).toBe(false)
  117. })
  118. it('should clear payment states', () => {
  119. const stateManager = PaymentStateManager.getInstance()
  120. stateManager.setPaymentState(123, PaymentStatus.PROCESSING)
  121. stateManager.clearPaymentState(123)
  122. expect(stateManager.getPaymentState(123)).toBeUndefined()
  123. })
  124. })
  125. describe('retryPayment', () => {
  126. it('should succeed on first attempt', async () => {
  127. // 直接模拟 requestWechatPayment 函数返回成功结果
  128. const mockRequestWechatPayment = jest.fn()
  129. mockRequestWechatPayment.mockResolvedValueOnce({
  130. success: true,
  131. type: 'success',
  132. result: {}
  133. })
  134. const paymentFn = async () => {
  135. return await mockRequestWechatPayment()
  136. }
  137. const result = await retryPayment(paymentFn)
  138. expect(result.success).toBe(true)
  139. expect(result.type).toBe('success')
  140. expect(mockRequestWechatPayment).toHaveBeenCalledTimes(1)
  141. })
  142. it('should retry on failure', async () => {
  143. // 直接模拟 requestWechatPayment 函数,第一次失败,第二次成功
  144. const mockRequestWechatPayment = jest.fn()
  145. mockRequestWechatPayment
  146. .mockResolvedValueOnce({
  147. success: false,
  148. type: 'error',
  149. message: '网络错误'
  150. })
  151. .mockResolvedValueOnce({
  152. success: true,
  153. type: 'success',
  154. result: {}
  155. })
  156. const paymentFn = async () => {
  157. return await mockRequestWechatPayment()
  158. }
  159. const result = await retryPayment(paymentFn, 3, 10)
  160. expect(result.success).toBe(true)
  161. expect(result.type).toBe('success')
  162. expect(mockRequestWechatPayment).toHaveBeenCalledTimes(2)
  163. })
  164. it('should not retry on user cancellation', async () => {
  165. // 直接模拟 requestWechatPayment 函数返回用户取消结果
  166. const mockRequestWechatPayment = jest.fn()
  167. mockRequestWechatPayment.mockResolvedValueOnce({
  168. success: false,
  169. type: 'cancel',
  170. message: '用户取消支付'
  171. })
  172. const paymentFn = async () => {
  173. return await mockRequestWechatPayment()
  174. }
  175. const result = await retryPayment(paymentFn)
  176. expect(result.success).toBe(false)
  177. expect(result.type).toBe('cancel')
  178. expect(result.message).toBe('用户取消支付')
  179. expect(mockRequestWechatPayment).toHaveBeenCalledTimes(1)
  180. })
  181. })
  182. describe('PaymentRateLimiter', () => {
  183. it('should allow payments within rate limit', () => {
  184. const rateLimiter = PaymentRateLimiter.getInstance()
  185. for (let i = 0; i < 4; i++) {
  186. rateLimiter.recordAttempt(123)
  187. }
  188. const result = rateLimiter.isRateLimited(123)
  189. expect(result.limited).toBe(false)
  190. })
  191. it('should block payments exceeding rate limit', () => {
  192. const rateLimiter = PaymentRateLimiter.getInstance()
  193. for (let i = 0; i < 5; i++) {
  194. rateLimiter.recordAttempt(123)
  195. }
  196. const result = rateLimiter.isRateLimited(123)
  197. expect(result.limited).toBe(true)
  198. expect(result.remainingTime).toBeGreaterThan(0)
  199. })
  200. it('should clear attempts', () => {
  201. const rateLimiter = PaymentRateLimiter.getInstance()
  202. rateLimiter.recordAttempt(123)
  203. rateLimiter.clearAttempts(123)
  204. const result = rateLimiter.isRateLimited(123)
  205. expect(result.limited).toBe(false)
  206. })
  207. })
  208. describe('validateAmountConsistency', () => {
  209. it('should validate consistent amounts', () => {
  210. const result = validateAmountConsistency(100, 10000)
  211. expect(result.valid).toBe(true)
  212. })
  213. it('should detect inconsistent amounts', () => {
  214. const result = validateAmountConsistency(100, 9999)
  215. expect(result.valid).toBe(false)
  216. expect(result.reason).toContain('金额不一致')
  217. })
  218. })
  219. describe('generatePaymentParamsHash', () => {
  220. it('should generate consistent hash for same parameters', () => {
  221. const params = {
  222. timeStamp: '1234567890',
  223. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  224. package: 'prepay_id=wx1234567890',
  225. signType: 'RSA',
  226. paySign: 'signature'
  227. }
  228. const hash1 = generatePaymentParamsHash(params)
  229. const hash2 = generatePaymentParamsHash(params)
  230. expect(hash1).toBe(hash2)
  231. })
  232. it('should generate different hash for different parameters', () => {
  233. const params1 = {
  234. timeStamp: '1234567890',
  235. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  236. package: 'prepay_id=wx1234567890',
  237. signType: 'RSA',
  238. paySign: 'signature'
  239. }
  240. const params2 = {
  241. timeStamp: '1234567891', // 不同的时间戳
  242. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  243. package: 'prepay_id=wx1234567890',
  244. signType: 'RSA',
  245. paySign: 'signature'
  246. }
  247. const hash1 = generatePaymentParamsHash(params1)
  248. const hash2 = generatePaymentParamsHash(params2)
  249. expect(hash1).not.toBe(hash2)
  250. })
  251. })
  252. describe('verifyPaymentParamsIntegrity', () => {
  253. it('should verify identical parameters', () => {
  254. const originalParams = {
  255. timeStamp: '1234567890',
  256. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  257. package: 'prepay_id=wx1234567890',
  258. signType: 'RSA',
  259. paySign: 'signature'
  260. }
  261. const receivedParams = { ...originalParams }
  262. const result = verifyPaymentParamsIntegrity(originalParams, receivedParams)
  263. expect(result.valid).toBe(true)
  264. })
  265. it('should detect tampered parameters', () => {
  266. const originalParams = {
  267. timeStamp: '1234567890',
  268. nonceStr: 'abcdefghijklmnopqrstuvwxyz',
  269. package: 'prepay_id=wx1234567890',
  270. signType: 'RSA',
  271. paySign: 'signature'
  272. }
  273. const tamperedParams = {
  274. ...originalParams,
  275. package: 'prepay_id=wx9876543210' // 被篡改的预支付ID
  276. }
  277. const result = verifyPaymentParamsIntegrity(originalParams, tamperedParams)
  278. expect(result.valid).toBe(false)
  279. expect(result.reason).toContain('支付参数被篡改')
  280. })
  281. })
  282. })