payment.test.ts 10 KB

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