package com.xuekairui.gateway; import org.junit.jupiter.api.*; import org.springframework.http.MediaType; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*; import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*; /** * 支付模块接口测试 */ @DisplayName("支付模块接口测试") @TestMethodOrder(MethodOrderer.OrderAnnotation.class) class PaymentControllerTest extends GatewayBaseTest { // ==================== 支付方案 ==================== @Test @Order(1) @DisplayName("GET /api/payment/plans - 无需认证应返回方案列表") void getPlans_withoutAuth_shouldReturnPlans() throws Exception { mockMvc.perform(get("/api/payment/plans")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isArray()) .andExpect(jsonPath("$.data.length()").value(org.hamcrest.Matchers.greaterThanOrEqualTo(2))) .andExpect(jsonPath("$.data[0].planCode").exists()) .andExpect(jsonPath("$.data[0].planName").exists()); } @Test @Order(2) @DisplayName("GET /api/payment/plans - 方案应包含价格、会员等级等字段") void getPlans_shouldContainRequiredFields() throws Exception { mockMvc.perform(get("/api/payment/plans")) .andExpect(status().isOk()) .andExpect(jsonPath("$.data[0].price").isNumber()) .andExpect(jsonPath("$.data[0].membershipLevel").exists()) .andExpect(jsonPath("$.data[0].durationDays").isNumber()) .andExpect(jsonPath("$.data[0].planCode").exists()); } // ==================== 创建订单 ==================== @Test @Order(3) @DisplayName("POST /api/payment/order/create - 微信支付应返回订单和二维码") void createOrder_wechat_shouldReturnQrCode() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(post("/api/payment/order/create") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"WECHAT\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.orderNo").exists()) .andExpect(jsonPath("$.data.planName").exists()) .andExpect(jsonPath("$.data.amount").isNumber()) .andExpect(jsonPath("$.data.channel").value("WECHAT")) .andExpect(jsonPath("$.data.status").value("PENDING")) .andExpect(jsonPath("$.data.qrCodeBase64").isString()) .andExpect(jsonPath("$.data.expireTime").exists()); } @Test @Order(4) @DisplayName("POST /api/payment/order/create - 支付宝应返回订单和二维码") void createOrder_alipay_shouldReturnQrCode() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(post("/api/payment/order/create") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"ALIPAY\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.planName").exists()) .andExpect(jsonPath("$.data.amount").isNumber()) .andExpect(jsonPath("$.data.channel").value("ALIPAY")); } @Test @Order(5) @DisplayName("POST /api/payment/order/create - 无效渠道应返回错误") void createOrder_invalidChannel_shouldReturnError() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(post("/api/payment/order/create") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"INVALID\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(2004)); } @Test @Order(6) @DisplayName("POST /api/payment/order/create - 无Token应返回401") void createOrder_withoutToken_shouldReturn401() throws Exception { mockMvc.perform(post("/api/payment/order/create") .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"WECHAT\"}")) .andExpect(status().isUnauthorized()); } // ==================== 查询订单 ==================== @Test @Order(7) @DisplayName("GET /api/payment/order/{orderNo} - 查询存在的订单应返回详情") void queryOrder_existing_shouldReturnDetail() throws Exception { String token = generateAccessToken(1L, "13800138000"); // 先创建订单 var result = mockMvc.perform(post("/api/payment/order/create") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"WECHAT\"}")) .andExpect(status().isOk()) .andReturn(); String orderNo = new com.fasterxml.jackson.databind.ObjectMapper() .readTree(result.getResponse().getContentAsString()) .get("data").get("orderNo").asText(); // 再查询 mockMvc.perform(get("/api/payment/order/" + orderNo) .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.orderNo").value(orderNo)); } @Test @Order(8) @DisplayName("GET /api/payment/order/{orderNo} - 不存在的订单应返回错误") void queryOrder_nonexistent_shouldReturnError() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(get("/api/payment/order/OP_NOTEXIST") .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(2001)); } // ==================== 模拟支付 ==================== @Test @Order(9) @DisplayName("POST /api/payment/order/{orderNo}/pay - 模拟支付应成功并激活会员") void mockPay_shouldSucceedAndActivateMembership() throws Exception { String token = generateAccessToken(1L, "13800138000"); // 创建订单 var result = mockMvc.perform(post("/api/payment/order/create") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"WECHAT\"}")) .andReturn(); String orderNo = new com.fasterxml.jackson.databind.ObjectMapper() .readTree(result.getResponse().getContentAsString()) .get("data").get("orderNo").asText(); // 模拟支付 mockMvc.perform(post("/api/payment/order/" + orderNo + "/pay") .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.status").value("PAID")) .andExpect(jsonPath("$.data.planName").exists()) .andExpect(jsonPath("$.data.membershipLevel").exists()) .andExpect(jsonPath("$.data.paidTime").exists()); } @Test @Order(10) @DisplayName("POST /api/payment/order/{orderNo}/pay - 已支付订单不能再次支付") void mockPay_alreadyPaid_shouldReturnError() throws Exception { String token = generateAccessToken(1L, "13800138000"); var result = mockMvc.perform(post("/api/payment/order/create") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planId\":1,\"channel\":\"WECHAT\"}")) .andReturn(); String orderNo = new com.fasterxml.jackson.databind.ObjectMapper() .readTree(result.getResponse().getContentAsString()) .get("data").get("orderNo").asText(); // 第一次支付 mockMvc.perform(post("/api/payment/order/" + orderNo + "/pay") .header("Authorization", "Bearer " + token)); // 第二次支付应返回已支付错误 mockMvc.perform(post("/api/payment/order/" + orderNo + "/pay") .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(2005)); } // ==================== 方案管理(需管理员权限,当前SecurityConfig中/admin/payment/**已纳入/admin/**保护) ==================== @Test @Order(12) @DisplayName("GET /api/payment/orders - 查询用户订单列表") void listMyOrders_shouldReturnList() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(get("/api/payment/orders") .header("Authorization", "Bearer " + token)) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data").isArray()); } // ==================== 管理端支付方案管理 ==================== @Test @Order(20) @DisplayName("POST /api/admin/payment/plans - 管理员创建支付方案") void createPlan_asAdmin_shouldSucceed() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(post("/api/admin/payment/plans") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planCode\":\"BIENNIAL\",\"planName\":\"两年卡\",\"price\":1599,\"originalPrice\":1798,\"durationDays\":730,\"membershipLevel\":\"PRO\",\"tag\":\"省11%\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.planCode").value("BIENNIAL")) .andExpect(jsonPath("$.data.planName").value("两年卡")); } @Test @Order(21) @DisplayName("PUT /api/admin/payment/plans/{id} - 管理员更新支付方案") void updatePlan_asAdmin_shouldSucceed() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(put("/api/admin/payment/plans/1") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planCode\":\"MONTHLY\",\"planName\":\"月卡(更新)\",\"price\":109,\"originalPrice\":109,\"durationDays\":30,\"membershipLevel\":\"PRO\"}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)) .andExpect(jsonPath("$.data.planName").value("月卡(更新)")); } @Test @Order(22) @DisplayName("PUT /api/admin/payment/plans/{id}/status - 切换方案上架/下架") void togglePlanStatus_asAdmin_shouldSucceed() throws Exception { String token = generateAccessToken(1L, "13800138000"); mockMvc.perform(put("/api/admin/payment/plans/1/status") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"status\":0}")) .andExpect(status().isOk()) .andExpect(jsonPath("$.code").value(200)); } @Test @Order(23) @DisplayName("POST /api/admin/payment/plans - 无Token应返回401") void createPlan_withoutToken_shouldReturn401() throws Exception { mockMvc.perform(post("/api/admin/payment/plans") .contentType(MediaType.APPLICATION_JSON) .content("{\"planCode\":\"TEST\",\"planName\":\"测试\",\"price\":1,\"durationDays\":1,\"membershipLevel\":\"PRO\"}")) .andExpect(status().isUnauthorized()); } @Test @Order(24) @DisplayName("PUT /api/admin/payment/plans/{id} - 普通用户应返回403") void updatePlan_asNormalUser_shouldReturn403() throws Exception { String token = generateAccessToken(1L, "13800138000", "USER"); mockMvc.perform(put("/api/admin/payment/plans/1") .header("Authorization", "Bearer " + token) .contentType(MediaType.APPLICATION_JSON) .content("{\"planCode\":\"MONTHLY\",\"planName\":\"月卡\",\"price\":99,\"durationDays\":30,\"membershipLevel\":\"PRO\"}")) .andExpect(status().isForbidden()); } // ==================== 支付回调 ==================== @Test @Order(30) @DisplayName("POST /api/payment/callback/wechat - 缺少订单号应返回FAIL") void wechatCallback_noOrderNo_shouldReturnFail() throws Exception { mockMvc.perform(post("/api/payment/callback/wechat") .contentType(MediaType.APPLICATION_FORM_URLENCODED)) .andExpect(status().isOk()) .andExpect(content().string("FAIL")); } @Test @Order(31) @DisplayName("POST /api/payment/callback/alipay - 缺少订单号应返回fail") void alipayCallback_noOrderNo_shouldReturnFail() throws Exception { mockMvc.perform(post("/api/payment/callback/alipay") .contentType(MediaType.APPLICATION_FORM_URLENCODED)) .andExpect(status().isOk()) .andExpect(content().string("fail")); } }