|
|
@@ -1,400 +0,0 @@
|
|
|
-package com.etotem.cfc.service;
|
|
|
-
|
|
|
-import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertNotNull;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertNull;
|
|
|
-import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
-import static org.mockito.Mockito.times;
|
|
|
-import static org.mockito.Mockito.verify;
|
|
|
-import static org.mockito.Mockito.when;
|
|
|
-
|
|
|
-import java.lang.reflect.Field;
|
|
|
-import java.util.HashMap;
|
|
|
-import java.util.LinkedHashMap;
|
|
|
-import java.util.Map;
|
|
|
-
|
|
|
-import org.junit.jupiter.api.Test;
|
|
|
-import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
-import org.mockito.InjectMocks;
|
|
|
-import org.mockito.Mock;
|
|
|
-import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
-
|
|
|
-import com.alibaba.fastjson.JSON;
|
|
|
-import com.etotem.cfc.common.HmacSignUtil;
|
|
|
-import com.etotem.cfc.dto.VirtualPayParamsDTO;
|
|
|
-import com.etotem.cfc.entity.AssessmentOrder;
|
|
|
-import com.etotem.cfc.entity.MemberSubscriptionOrder;
|
|
|
-import com.etotem.cfc.entity.PaymentOrder;
|
|
|
-import com.etotem.cfc.entity.VirtualGoodsConfig;
|
|
|
-
|
|
|
-@ExtendWith(MockitoExtension.class)
|
|
|
-class VirtualPayServiceTest {
|
|
|
-
|
|
|
- @Mock
|
|
|
- private AssessmentOrderService assessmentOrderService;
|
|
|
-
|
|
|
- @Mock
|
|
|
- private MembershipService membershipService;
|
|
|
-
|
|
|
- @Mock
|
|
|
- private MemberSubscriptionService memberSubscriptionService;
|
|
|
-
|
|
|
- @Mock
|
|
|
- private VirtualGoodsConfigService virtualGoodsConfigService;
|
|
|
-
|
|
|
- @Mock
|
|
|
- private WechatService wechatService;
|
|
|
-
|
|
|
- @InjectMocks
|
|
|
- private VirtualPayService virtualPayService;
|
|
|
-
|
|
|
- @Test
|
|
|
- void hmacSha256Hex_matchesRfc4231TestVector() {
|
|
|
- // RFC 4231 Test Case 2 标准测试向量
|
|
|
- String actual = HmacSignUtil.hmacSha256Hex("Jefe", "what do ya want for nothing?");
|
|
|
- assertEquals("5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", actual);
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generatePayParams_returnsCompleteDto() throws Exception {
|
|
|
- // 纯单测环境不注入 Spring @Value,通过反射填充配置字段
|
|
|
- setField("offerId", "1450607643");
|
|
|
- setField("appKey", "test_app_key");
|
|
|
- setField("env", 0);
|
|
|
-
|
|
|
- VirtualGoodsConfig config = new VirtualGoodsConfig();
|
|
|
- config.setProductId("offer_1001");
|
|
|
-
|
|
|
- VirtualPayParamsDTO dto = virtualPayService.generatePayParams(
|
|
|
- config, "A20260802001", 9900, "五维测评·基础版", "ASSESSMENT_PACKAGE:1", "test_session_key");
|
|
|
-
|
|
|
- assertNotNull(dto);
|
|
|
- assertNotNull(dto.getOrderInfo());
|
|
|
- assertEquals("1450607643", dto.getOrderInfo().get("offerId"));
|
|
|
- assertEquals(1, dto.getOrderInfo().get("buyQuantity"));
|
|
|
- assertEquals(0, dto.getOrderInfo().get("env"));
|
|
|
- assertEquals("CNY", dto.getOrderInfo().get("currencyType"));
|
|
|
- assertEquals("offer_1001", dto.getOrderInfo().get("productId"));
|
|
|
- assertEquals(9900, dto.getOrderInfo().get("goodsPrice"));
|
|
|
- assertEquals("A20260802001", dto.getOrderInfo().get("outTradeNo"));
|
|
|
- assertEquals("ASSESSMENT_PACKAGE:1", dto.getOrderInfo().get("attach"));
|
|
|
- assertEquals(0, dto.getEnv());
|
|
|
- assertEquals("HMAC-SHA256", dto.getSignType());
|
|
|
- assertNotNull(dto.getExtInfo());
|
|
|
- assertNotNull(dto.getSign());
|
|
|
- assertFalse(dto.getSign().isEmpty());
|
|
|
- assertNotNull(dto.getSignature());
|
|
|
- assertFalse(dto.getSignature().isEmpty());
|
|
|
- // 双签名值不同(paySig 用 appKey、signature 用 sessionKey)
|
|
|
- org.junit.jupiter.api.Assertions.assertNotEquals(dto.getSign(), dto.getSignature());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleDeliverNotify_assessmentPrefixRoutesToHandleVirtualPaySuccess() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("OutTradeNo", "A20260802001");
|
|
|
- payload.put("TransactionId", "WX1234567890");
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- assertEquals("success", result.get("ErrMsg"));
|
|
|
- verify(assessmentOrderService, times(1)).handleVirtualPaySuccess("A20260802001", "WX1234567890");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleDeliverNotify_missingOutTradeNoReturnsAck() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("TransactionId", "WX1234567890");
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- verify(assessmentOrderService, times(0)).paySuccess(org.mockito.ArgumentMatchers.any(),
|
|
|
- org.mockito.ArgumentMatchers.any(), org.mockito.ArgumentMatchers.any());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleRefundNotify_assessmentPrefixCallsRefundOrder() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("OutTradeNo", "A20260802001");
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleRefundNotify(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- assertEquals("success", result.get("ErrMsg"));
|
|
|
- verify(assessmentOrderService, times(1)).refundOrder("A20260802001");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generatePayParams_signsOfficialJsonSignData() throws Exception {
|
|
|
- // 对照官方签名规则:
|
|
|
- // signData = orderInfo 的 JSON 串(camelCase 官方字段、插入序,与前端 JSON.stringify 逐字节一致)
|
|
|
- // paySig = hmacSha256Hex(appKey, "requestVirtualPayment&" + signData)
|
|
|
- // signature = hmacSha256Hex(sessionKey, signData)
|
|
|
- setField("offerId", "1450607643");
|
|
|
- setField("appKey", "test_app_key");
|
|
|
- setField("env", 0);
|
|
|
-
|
|
|
- VirtualGoodsConfig config = new VirtualGoodsConfig();
|
|
|
- config.setProductId("offer_1001");
|
|
|
-
|
|
|
- VirtualPayParamsDTO dto = virtualPayService.generatePayParams(
|
|
|
- config, "A20260802001", 9900, "五维测评·基础版", "ASSESSMENT_PACKAGE:1", "test_session_key");
|
|
|
-
|
|
|
- // 期望 signData:官方 camelCase 字段、插入序序列化(锁定格式,防回归到旧字典序拼接)
|
|
|
- String expectedSignData = "{\"offerId\":\"1450607643\",\"buyQuantity\":1,\"env\":0,"
|
|
|
- + "\"currencyType\":\"CNY\",\"productId\":\"offer_1001\",\"goodsPrice\":9900,"
|
|
|
- + "\"outTradeNo\":\"A20260802001\",\"attach\":\"ASSESSMENT_PACKAGE:1\"}";
|
|
|
- assertEquals(expectedSignData, JSON.toJSONString(dto.getOrderInfo()));
|
|
|
- // paySig / signature 均基于同一 JSON 串(与前端实际发送的 signData 逐字节一致)
|
|
|
- assertEquals(HmacSignUtil.hmacSha256Hex("test_app_key", "requestVirtualPayment&" + expectedSignData), dto.getSign());
|
|
|
- assertEquals(HmacSignUtil.hmacSha256Hex("test_session_key", expectedSignData), dto.getSignature());
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleDeliverNotify_membershipPrefixRoutesToProcessPaymentCallback() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("OutTradeNo", "ORD20260802001");
|
|
|
- payload.put("TransactionId", "WX1234567890");
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- assertEquals("success", result.get("ErrMsg"));
|
|
|
- verify(membershipService, times(1)).processPaymentCallback("ORD20260802001", "WX1234567890", "virtual");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generateMembershipPayParams_returnsDtoForConfiguredLevel() throws Exception {
|
|
|
- setField("offerId", "1450607643");
|
|
|
- setField("appKey", "test_app_key");
|
|
|
- setField("env", 0);
|
|
|
-
|
|
|
- PaymentOrder order = new PaymentOrder();
|
|
|
- order.setOrderNo("ORD20260802001");
|
|
|
- order.setLevelCode("FAMILY");
|
|
|
- order.setAmount(131400);
|
|
|
- order.setStatus("pending");
|
|
|
- when(membershipService.getByOrderNo("ORD20260802001")).thenReturn(order);
|
|
|
-
|
|
|
- VirtualGoodsConfig config = new VirtualGoodsConfig();
|
|
|
- config.setProductId("offer_m1");
|
|
|
- config.setGoodsName("家庭会员年卡");
|
|
|
- when(virtualGoodsConfigService.getEnabled("MEMBERSHIP", "FAMILY")).thenReturn(config);
|
|
|
-
|
|
|
- VirtualPayParamsDTO dto = virtualPayService.generateMembershipPayParams("ORD20260802001", "test_session_key");
|
|
|
-
|
|
|
- assertNotNull(dto);
|
|
|
- assertEquals("1450607643", dto.getOrderInfo().get("offerId"));
|
|
|
- assertEquals("offer_m1", dto.getOrderInfo().get("productId"));
|
|
|
- assertEquals(131400, dto.getOrderInfo().get("goodsPrice"));
|
|
|
- assertEquals("ORD20260802001", dto.getOrderInfo().get("outTradeNo"));
|
|
|
- assertEquals("membership:FAMILY", dto.getOrderInfo().get("attach"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generateMembershipPayParams_returnsNullWhenConfigMissing() {
|
|
|
- PaymentOrder order = new PaymentOrder();
|
|
|
- order.setOrderNo("ORD20260802002");
|
|
|
- order.setLevelCode("PREMIUM");
|
|
|
- order.setAmount(1316800);
|
|
|
- order.setStatus("pending");
|
|
|
- when(membershipService.getByOrderNo("ORD20260802002")).thenReturn(order);
|
|
|
- when(virtualGoodsConfigService.getEnabled("MEMBERSHIP", "PREMIUM")).thenReturn(null);
|
|
|
-
|
|
|
- assertNull(virtualPayService.generateMembershipPayParams("ORD20260802002", "test_session_key"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleDeliverNotify_subscriptionPrefixRoutesToActivateSubscription() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("OutTradeNo", "SUB20260802001");
|
|
|
- payload.put("TransactionId", "WX1234567890");
|
|
|
-
|
|
|
- MemberSubscriptionOrder order = new MemberSubscriptionOrder();
|
|
|
- order.setOrderNo("SUB20260802001");
|
|
|
- order.setFamilyId(1L);
|
|
|
- order.setLevel("L1");
|
|
|
- order.setAmount(36500);
|
|
|
- order.setStatus("pending");
|
|
|
- when(memberSubscriptionService.getByOrderNo("SUB20260802001")).thenReturn(order);
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleDeliverNotify(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- assertEquals("success", result.get("ErrMsg"));
|
|
|
- verify(memberSubscriptionService, times(1))
|
|
|
- .activateSubscription(1L, "L1", 36500, "virtual", "WX1234567890", "SUB20260802001");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generateSubscriptionPayParams_returnsDtoForConfiguredLevel() throws Exception {
|
|
|
- setField("offerId", "1450607643");
|
|
|
- setField("appKey", "test_app_key");
|
|
|
- setField("env", 0);
|
|
|
-
|
|
|
- MemberSubscriptionOrder order = new MemberSubscriptionOrder();
|
|
|
- order.setOrderNo("SUB20260802001");
|
|
|
- order.setLevel("L1");
|
|
|
- order.setAmount(36500);
|
|
|
- order.setStatus("pending");
|
|
|
- when(memberSubscriptionService.getByOrderNo("SUB20260802001")).thenReturn(order);
|
|
|
-
|
|
|
- VirtualGoodsConfig config = new VirtualGoodsConfig();
|
|
|
- config.setProductId("offer_sub_l1");
|
|
|
- config.setGoodsName("一生一世L1");
|
|
|
- when(virtualGoodsConfigService.getEnabled("SUBSCRIPTION", "L1")).thenReturn(config);
|
|
|
-
|
|
|
- VirtualPayParamsDTO dto = virtualPayService.generateSubscriptionPayParams("SUB20260802001", "test_session_key");
|
|
|
-
|
|
|
- assertNotNull(dto);
|
|
|
- assertEquals("1450607643", dto.getOrderInfo().get("offerId"));
|
|
|
- assertEquals("offer_sub_l1", dto.getOrderInfo().get("productId"));
|
|
|
- assertEquals(36500, dto.getOrderInfo().get("goodsPrice"));
|
|
|
- assertEquals("SUB20260802001", dto.getOrderInfo().get("outTradeNo"));
|
|
|
- assertEquals("subscription:L1", dto.getOrderInfo().get("attach"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void generateSubscriptionPayParams_returnsNullWhenConfigMissing() {
|
|
|
- MemberSubscriptionOrder order = new MemberSubscriptionOrder();
|
|
|
- order.setOrderNo("SUB20260802002");
|
|
|
- order.setLevel("L2");
|
|
|
- order.setAmount(131400);
|
|
|
- order.setStatus("pending");
|
|
|
- when(memberSubscriptionService.getByOrderNo("SUB20260802002")).thenReturn(order);
|
|
|
- when(virtualGoodsConfigService.getEnabled("SUBSCRIPTION", "L2")).thenReturn(null);
|
|
|
-
|
|
|
- assertNull(virtualPayService.generateSubscriptionPayParams("SUB20260802002", "test_session_key"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void queryOrder_testModeReturnsUnpaidWithoutHttp() throws Exception {
|
|
|
- setField("testMode", true);
|
|
|
- setField("env", 0);
|
|
|
- VirtualPayService.QueryOrderResult result = virtualPayService.queryOrder("A20260802001", "test_openid");
|
|
|
- assertNotNull(result);
|
|
|
- assertFalse(result.paid);
|
|
|
- assertEquals(1, result.status);
|
|
|
- // 测试模式不触发 HTTP(wechatService 未被调用)
|
|
|
- verify(wechatService, times(0)).getAccessToken();
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void queryOrder_buildsPaySigWithUriAndRawBody() throws Exception {
|
|
|
- // 对照官方规则: pay_sig = hmacSha256Hex(appKey, "/xpay/query_order&" + post_body)
|
|
|
- setField("appKey", "test_app_key");
|
|
|
- setField("env", 0);
|
|
|
- // 验证签名拼接规则:uri 前缀 + 原始 body(与 generatePayParams 的 buildSignData 不同!)
|
|
|
- String postBody = JSON.toJSONString(new LinkedHashMap<String, Object>() {{
|
|
|
- put("openid", "test_openid");
|
|
|
- put("env", 0);
|
|
|
- put("order_id", "A20260802001");
|
|
|
- put("wx_order_id", "");
|
|
|
- }});
|
|
|
- String expectedPaySig = HmacSignUtil.hmacSha256Hex("test_app_key", "/xpay/query_order&" + postBody);
|
|
|
- // package-private buildQueryOrderPaySig 直接断言签名构建逻辑
|
|
|
- assertEquals(expectedPaySig, virtualPayService.buildQueryOrderPaySig(postBody));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleRefundNotify_membershipPrefixCallsRefundByOrderNo() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("OutTradeNo", "ORD20260804001");
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleRefundNotify(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- assertEquals("success", result.get("ErrMsg"));
|
|
|
- verify(membershipService, times(1)).refundByOrderNo("ORD20260804001");
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleIosRefundQuery_paidOrderWithoutDeliveryAgreesRefund() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("pay_order_id", "ORD20260804001");
|
|
|
- payload.put("refund_request_reason", "UNINTENDED_PURCHASE");
|
|
|
- payload.put("provide_status", "0");
|
|
|
-
|
|
|
- PaymentOrder order = new PaymentOrder();
|
|
|
- order.setOrderNo("ORD20260804001");
|
|
|
- order.setLevelCode("FAMILY");
|
|
|
- order.setStatus("paid");
|
|
|
- when(membershipService.getByOrderNo("ORD20260804001")).thenReturn(order);
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleIosRefundQuery(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- assertNotNull(result.get("IosRefundQueryResponse"));
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- Map<String, Object> resp = (Map<String, Object>) result.get("IosRefundQueryResponse");
|
|
|
- assertEquals(0, resp.get("result_code")); // 建议退款
|
|
|
- assertEquals("同意退款", resp.get("result_info"));
|
|
|
- assertNotNull(resp.get("evidence"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleIosRefundQuery_deliveredOrderRefusesRefund() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("pay_order_id", "A20260804001");
|
|
|
- payload.put("provide_status", "1");
|
|
|
-
|
|
|
- AssessmentOrder order = new AssessmentOrder();
|
|
|
- order.setOrderNo("A20260804001");
|
|
|
- order.setStatus("paid");
|
|
|
- when(assessmentOrderService.getByOrderNo("A20260804001")).thenReturn(order);
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleIosRefundQuery(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- Map<String, Object> resp = (Map<String, Object>) result.get("IosRefundQueryResponse");
|
|
|
- assertEquals(1, resp.get("result_code")); // 拒绝退款
|
|
|
- assertEquals("拒绝退款", resp.get("result_info"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleIosRefundQuery_unknownOrderRefusesRefund() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("pay_order_id", "ORD_NONEXIST");
|
|
|
- payload.put("provide_status", "0");
|
|
|
-
|
|
|
- when(membershipService.getByOrderNo("ORD_NONEXIST")).thenReturn(null);
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleIosRefundQuery(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- Map<String, Object> resp = (Map<String, Object>) result.get("IosRefundQueryResponse");
|
|
|
- assertEquals(1, resp.get("result_code")); // 拒绝退款
|
|
|
- assertTrue(resp.get("evidence").toString().contains("订单不存在"));
|
|
|
- }
|
|
|
-
|
|
|
- @Test
|
|
|
- void handleIosRefundQuery_alreadyRefundedAgreesRefund() {
|
|
|
- Map<String, Object> payload = new HashMap<>();
|
|
|
- payload.put("pay_order_id", "SUB20260804001");
|
|
|
- payload.put("provide_status", "1");
|
|
|
-
|
|
|
- MemberSubscriptionOrder order = new MemberSubscriptionOrder();
|
|
|
- order.setOrderNo("SUB20260804001");
|
|
|
- order.setStatus("refunded");
|
|
|
- when(memberSubscriptionService.getByOrderNo("SUB20260804001")).thenReturn(order);
|
|
|
-
|
|
|
- Map<String, Object> result = virtualPayService.handleIosRefundQuery(payload);
|
|
|
-
|
|
|
- assertEquals(0, result.get("ErrCode"));
|
|
|
- @SuppressWarnings("unchecked")
|
|
|
- Map<String, Object> resp = (Map<String, Object>) result.get("IosRefundQueryResponse");
|
|
|
- assertEquals(0, resp.get("result_code")); // 建议退款(已退款)
|
|
|
- assertTrue(resp.get("evidence").toString().contains("已退款"));
|
|
|
- }
|
|
|
-
|
|
|
- private void setField(String name, Object value) throws Exception {
|
|
|
- Field field = VirtualPayService.class.getDeclaredField(name);
|
|
|
- field.setAccessible(true);
|
|
|
- field.set(virtualPayService, value);
|
|
|
- }
|
|
|
-}
|