PaymentControllerTest.java 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295
  1. package com.xuekairui.gateway;
  2. import org.junit.jupiter.api.*;
  3. import org.springframework.http.MediaType;
  4. import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
  5. import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
  6. /**
  7. * 支付模块接口测试
  8. */
  9. @DisplayName("支付模块接口测试")
  10. @TestMethodOrder(MethodOrderer.OrderAnnotation.class)
  11. class PaymentControllerTest extends GatewayBaseTest {
  12. // ==================== 支付方案 ====================
  13. @Test
  14. @Order(1)
  15. @DisplayName("GET /api/payment/plans - 无需认证应返回方案列表")
  16. void getPlans_withoutAuth_shouldReturnPlans() throws Exception {
  17. mockMvc.perform(get("/api/payment/plans"))
  18. .andExpect(status().isOk())
  19. .andExpect(jsonPath("$.code").value(200))
  20. .andExpect(jsonPath("$.data").isArray())
  21. .andExpect(jsonPath("$.data.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(2)))
  22. .andExpect(jsonPath("$.data[0].planCode").exists())
  23. .andExpect(jsonPath("$.data[0].planName").exists());
  24. }
  25. @Test
  26. @Order(2)
  27. @DisplayName("GET /api/payment/plans - 方案应包含价格、会员等级等字段")
  28. void getPlans_shouldContainRequiredFields() throws Exception {
  29. mockMvc.perform(get("/api/payment/plans"))
  30. .andExpect(status().isOk())
  31. .andExpect(jsonPath("$.data[0].price").isNumber())
  32. .andExpect(jsonPath("$.data[0].membershipLevel").exists())
  33. .andExpect(jsonPath("$.data[0].durationDays").isNumber())
  34. .andExpect(jsonPath("$.data[0].planCode").exists());
  35. }
  36. // ==================== 创建订单 ====================
  37. @Test
  38. @Order(3)
  39. @DisplayName("POST /api/payment/order/create - 微信支付应返回订单和二维码")
  40. void createOrder_wechat_shouldReturnQrCode() throws Exception {
  41. String token = generateAccessToken(1L, "13800138000");
  42. mockMvc.perform(post("/api/payment/order/create")
  43. .header("Authorization", "Bearer " + token)
  44. .contentType(MediaType.APPLICATION_JSON)
  45. .content("{\"planId\":1,\"channel\":\"WECHAT\"}"))
  46. .andExpect(status().isOk())
  47. .andExpect(jsonPath("$.code").value(200))
  48. .andExpect(jsonPath("$.data.orderNo").exists())
  49. .andExpect(jsonPath("$.data.planName").exists())
  50. .andExpect(jsonPath("$.data.amount").isNumber())
  51. .andExpect(jsonPath("$.data.channel").value("WECHAT"))
  52. .andExpect(jsonPath("$.data.status").value("PENDING"))
  53. .andExpect(jsonPath("$.data.qrCodeBase64").isString())
  54. .andExpect(jsonPath("$.data.expireTime").exists());
  55. }
  56. @Test
  57. @Order(4)
  58. @DisplayName("POST /api/payment/order/create - 支付宝应返回订单和二维码")
  59. void createOrder_alipay_shouldReturnQrCode() throws Exception {
  60. String token = generateAccessToken(1L, "13800138000");
  61. mockMvc.perform(post("/api/payment/order/create")
  62. .header("Authorization", "Bearer " + token)
  63. .contentType(MediaType.APPLICATION_JSON)
  64. .content("{\"planId\":1,\"channel\":\"ALIPAY\"}"))
  65. .andExpect(status().isOk())
  66. .andExpect(jsonPath("$.code").value(200))
  67. .andExpect(jsonPath("$.data.planName").exists())
  68. .andExpect(jsonPath("$.data.amount").isNumber())
  69. .andExpect(jsonPath("$.data.channel").value("ALIPAY"));
  70. }
  71. @Test
  72. @Order(5)
  73. @DisplayName("POST /api/payment/order/create - 无效渠道应返回错误")
  74. void createOrder_invalidChannel_shouldReturnError() throws Exception {
  75. String token = generateAccessToken(1L, "13800138000");
  76. mockMvc.perform(post("/api/payment/order/create")
  77. .header("Authorization", "Bearer " + token)
  78. .contentType(MediaType.APPLICATION_JSON)
  79. .content("{\"planId\":1,\"channel\":\"INVALID\"}"))
  80. .andExpect(status().isOk())
  81. .andExpect(jsonPath("$.code").value(2004));
  82. }
  83. @Test
  84. @Order(6)
  85. @DisplayName("POST /api/payment/order/create - 无Token应返回401")
  86. void createOrder_withoutToken_shouldReturn401() throws Exception {
  87. mockMvc.perform(post("/api/payment/order/create")
  88. .contentType(MediaType.APPLICATION_JSON)
  89. .content("{\"planId\":1,\"channel\":\"WECHAT\"}"))
  90. .andExpect(status().isUnauthorized());
  91. }
  92. // ==================== 查询订单 ====================
  93. @Test
  94. @Order(7)
  95. @DisplayName("GET /api/payment/order/{orderNo} - 查询存在的订单应返回详情")
  96. void queryOrder_existing_shouldReturnDetail() throws Exception {
  97. String token = generateAccessToken(1L, "13800138000");
  98. // 先创建订单
  99. var result = mockMvc.perform(post("/api/payment/order/create")
  100. .header("Authorization", "Bearer " + token)
  101. .contentType(MediaType.APPLICATION_JSON)
  102. .content("{\"planId\":1,\"channel\":\"WECHAT\"}"))
  103. .andExpect(status().isOk())
  104. .andReturn();
  105. String orderNo = new com.fasterxml.jackson.databind.ObjectMapper()
  106. .readTree(result.getResponse().getContentAsString())
  107. .get("data").get("orderNo").asText();
  108. // 再查询
  109. mockMvc.perform(get("/api/payment/order/" + orderNo)
  110. .header("Authorization", "Bearer " + token))
  111. .andExpect(status().isOk())
  112. .andExpect(jsonPath("$.code").value(200))
  113. .andExpect(jsonPath("$.data.orderNo").value(orderNo));
  114. }
  115. @Test
  116. @Order(8)
  117. @DisplayName("GET /api/payment/order/{orderNo} - 不存在的订单应返回错误")
  118. void queryOrder_nonexistent_shouldReturnError() throws Exception {
  119. String token = generateAccessToken(1L, "13800138000");
  120. mockMvc.perform(get("/api/payment/order/OP_NOTEXIST")
  121. .header("Authorization", "Bearer " + token))
  122. .andExpect(status().isOk())
  123. .andExpect(jsonPath("$.code").value(2001));
  124. }
  125. // ==================== 模拟支付 ====================
  126. @Test
  127. @Order(9)
  128. @DisplayName("POST /api/payment/order/{orderNo}/pay - 模拟支付应成功并激活会员")
  129. void mockPay_shouldSucceedAndActivateMembership() throws Exception {
  130. String token = generateAccessToken(1L, "13800138000");
  131. // 创建订单
  132. var result = mockMvc.perform(post("/api/payment/order/create")
  133. .header("Authorization", "Bearer " + token)
  134. .contentType(MediaType.APPLICATION_JSON)
  135. .content("{\"planId\":1,\"channel\":\"WECHAT\"}"))
  136. .andReturn();
  137. String orderNo = new com.fasterxml.jackson.databind.ObjectMapper()
  138. .readTree(result.getResponse().getContentAsString())
  139. .get("data").get("orderNo").asText();
  140. // 模拟支付
  141. mockMvc.perform(post("/api/payment/order/" + orderNo + "/pay")
  142. .header("Authorization", "Bearer " + token))
  143. .andExpect(status().isOk())
  144. .andExpect(jsonPath("$.code").value(200))
  145. .andExpect(jsonPath("$.data.status").value("PAID"))
  146. .andExpect(jsonPath("$.data.planName").exists())
  147. .andExpect(jsonPath("$.data.membershipLevel").exists())
  148. .andExpect(jsonPath("$.data.paidTime").exists());
  149. }
  150. @Test
  151. @Order(10)
  152. @DisplayName("POST /api/payment/order/{orderNo}/pay - 已支付订单不能再次支付")
  153. void mockPay_alreadyPaid_shouldReturnError() throws Exception {
  154. String token = generateAccessToken(1L, "13800138000");
  155. var result = mockMvc.perform(post("/api/payment/order/create")
  156. .header("Authorization", "Bearer " + token)
  157. .contentType(MediaType.APPLICATION_JSON)
  158. .content("{\"planId\":1,\"channel\":\"WECHAT\"}"))
  159. .andReturn();
  160. String orderNo = new com.fasterxml.jackson.databind.ObjectMapper()
  161. .readTree(result.getResponse().getContentAsString())
  162. .get("data").get("orderNo").asText();
  163. // 第一次支付
  164. mockMvc.perform(post("/api/payment/order/" + orderNo + "/pay")
  165. .header("Authorization", "Bearer " + token));
  166. // 第二次支付应返回已支付错误
  167. mockMvc.perform(post("/api/payment/order/" + orderNo + "/pay")
  168. .header("Authorization", "Bearer " + token))
  169. .andExpect(status().isOk())
  170. .andExpect(jsonPath("$.code").value(2005));
  171. }
  172. // ==================== 方案管理(需管理员权限,当前SecurityConfig中/admin/payment/**已纳入/admin/**保护) ====================
  173. @Test
  174. @Order(12)
  175. @DisplayName("GET /api/payment/orders - 查询用户订单列表")
  176. void listMyOrders_shouldReturnList() throws Exception {
  177. String token = generateAccessToken(1L, "13800138000");
  178. mockMvc.perform(get("/api/payment/orders")
  179. .header("Authorization", "Bearer " + token))
  180. .andExpect(status().isOk())
  181. .andExpect(jsonPath("$.code").value(200))
  182. .andExpect(jsonPath("$.data").isArray());
  183. }
  184. // ==================== 管理端支付方案管理 ====================
  185. @Test
  186. @Order(20)
  187. @DisplayName("POST /api/admin/payment/plans - 管理员创建支付方案")
  188. void createPlan_asAdmin_shouldSucceed() throws Exception {
  189. String token = generateAccessToken(1L, "13800138000");
  190. mockMvc.perform(post("/api/admin/payment/plans")
  191. .header("Authorization", "Bearer " + token)
  192. .contentType(MediaType.APPLICATION_JSON)
  193. .content("{\"planCode\":\"BIENNIAL\",\"planName\":\"两年卡\",\"price\":1599,\"originalPrice\":1798,\"durationDays\":730,\"membershipLevel\":\"PRO\",\"tag\":\"省11%\"}"))
  194. .andExpect(status().isOk())
  195. .andExpect(jsonPath("$.code").value(200))
  196. .andExpect(jsonPath("$.data.planCode").value("BIENNIAL"))
  197. .andExpect(jsonPath("$.data.planName").value("两年卡"));
  198. }
  199. @Test
  200. @Order(21)
  201. @DisplayName("PUT /api/admin/payment/plans/{id} - 管理员更新支付方案")
  202. void updatePlan_asAdmin_shouldSucceed() throws Exception {
  203. String token = generateAccessToken(1L, "13800138000");
  204. mockMvc.perform(put("/api/admin/payment/plans/1")
  205. .header("Authorization", "Bearer " + token)
  206. .contentType(MediaType.APPLICATION_JSON)
  207. .content("{\"planCode\":\"MONTHLY\",\"planName\":\"月卡(更新)\",\"price\":109,\"originalPrice\":109,\"durationDays\":30,\"membershipLevel\":\"PRO\"}"))
  208. .andExpect(status().isOk())
  209. .andExpect(jsonPath("$.code").value(200))
  210. .andExpect(jsonPath("$.data.planName").value("月卡(更新)"));
  211. }
  212. @Test
  213. @Order(22)
  214. @DisplayName("PUT /api/admin/payment/plans/{id}/status - 切换方案上架/下架")
  215. void togglePlanStatus_asAdmin_shouldSucceed() throws Exception {
  216. String token = generateAccessToken(1L, "13800138000");
  217. mockMvc.perform(put("/api/admin/payment/plans/1/status")
  218. .header("Authorization", "Bearer " + token)
  219. .contentType(MediaType.APPLICATION_JSON)
  220. .content("{\"status\":0}"))
  221. .andExpect(status().isOk())
  222. .andExpect(jsonPath("$.code").value(200));
  223. }
  224. @Test
  225. @Order(23)
  226. @DisplayName("POST /api/admin/payment/plans - 无Token应返回401")
  227. void createPlan_withoutToken_shouldReturn401() throws Exception {
  228. mockMvc.perform(post("/api/admin/payment/plans")
  229. .contentType(MediaType.APPLICATION_JSON)
  230. .content("{\"planCode\":\"TEST\",\"planName\":\"测试\",\"price\":1,\"durationDays\":1,\"membershipLevel\":\"PRO\"}"))
  231. .andExpect(status().isUnauthorized());
  232. }
  233. @Test
  234. @Order(24)
  235. @DisplayName("PUT /api/admin/payment/plans/{id} - 普通用户应返回403")
  236. void updatePlan_asNormalUser_shouldReturn403() throws Exception {
  237. String token = generateAccessToken(1L, "13800138000", "USER");
  238. mockMvc.perform(put("/api/admin/payment/plans/1")
  239. .header("Authorization", "Bearer " + token)
  240. .contentType(MediaType.APPLICATION_JSON)
  241. .content("{\"planCode\":\"MONTHLY\",\"planName\":\"月卡\",\"price\":99,\"durationDays\":30,\"membershipLevel\":\"PRO\"}"))
  242. .andExpect(status().isForbidden());
  243. }
  244. // ==================== 支付回调 ====================
  245. @Test
  246. @Order(30)
  247. @DisplayName("POST /api/payment/callback/wechat - 缺少订单号应返回FAIL")
  248. void wechatCallback_noOrderNo_shouldReturnFail() throws Exception {
  249. mockMvc.perform(post("/api/payment/callback/wechat")
  250. .contentType(MediaType.APPLICATION_FORM_URLENCODED))
  251. .andExpect(status().isOk())
  252. .andExpect(content().string("FAIL"));
  253. }
  254. @Test
  255. @Order(31)
  256. @DisplayName("POST /api/payment/callback/alipay - 缺少订单号应返回fail")
  257. void alipayCallback_noOrderNo_shouldReturnFail() throws Exception {
  258. mockMvc.perform(post("/api/payment/callback/alipay")
  259. .contentType(MediaType.APPLICATION_FORM_URLENCODED))
  260. .andExpect(status().isOk())
  261. .andExpect(content().string("fail"));
  262. }
  263. }