payment.test.ts 10 KB

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