|
|
@@ -2,6 +2,7 @@ package com.payment.platform.module.payment.har.controller.open;
|
|
|
|
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
|
import com.payment.platform.module.payment.har.dto.HarOpenOrderQueryDTO;
|
|
|
+import com.payment.platform.module.payment.har.dto.HarOpenRefundDTO;
|
|
|
import com.payment.platform.module.payment.har.dto.HarOrderCreateDTO;
|
|
|
import com.payment.platform.module.payment.har.dto.HarShopCreateDTO;
|
|
|
import com.payment.platform.module.payment.har.entity.HarMerchantEntity;
|
|
|
@@ -14,6 +15,8 @@ import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
import org.mockito.Mock;
|
|
|
import org.mockito.junit.jupiter.MockitoExtension;
|
|
|
import org.springframework.mock.web.MockHttpServletRequest;
|
|
|
+import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
|
|
|
import static org.junit.jupiter.api.Assertions.*;
|
|
|
import static org.mockito.ArgumentMatchers.any;
|
|
|
@@ -148,4 +151,101 @@ class HarOpenControllerTest {
|
|
|
assertEquals(403, e.getCode(), "实测消息: " + e.getMessage());
|
|
|
verifyNoInteractions(orderService);
|
|
|
}
|
|
|
+
|
|
|
+ // ==================== 退款端点 ====================
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void refund_withoutApiKeyAuth_is401() {
|
|
|
+ request.removeAttribute("openapi.tenantId");
|
|
|
+ HarOpenRefundDTO dto = new HarOpenRefundDTO();
|
|
|
+ dto.setOrderNo("HAR1");
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> controller.refund(dto));
|
|
|
+
|
|
|
+ assertEquals(401, e.getCode(), "实测消息: " + e.getMessage());
|
|
|
+ verifyNoInteractions(orderService);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 闸里的 merchant_code 分支必须**被调用点真的传进来**(同 orderQuery 那条教训:
|
|
|
+ * 调用点写死 null,闸里那条分支在生产里就是死的,而 service 层用例全绿)。
|
|
|
+ *
|
|
|
+ * 少传它,多商户租户的退款会被 400「请指定 merchant_code」挡下,而请求体里
|
|
|
+ * 根本没有这个字段可填 —— 那是一条**照办不了**的提示。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void refund_forwardsMerchantCode_soMultiMerchantTenantsCanRefund() {
|
|
|
+ when(merchantService.requireByTenantAndCode(TENANT, "M700")).thenReturn(merchant("M700"));
|
|
|
+ HarOpenRefundDTO dto = new HarOpenRefundDTO();
|
|
|
+ dto.setMerchantCode("M700");
|
|
|
+ dto.setOrderNo("HAR1");
|
|
|
+
|
|
|
+ controller.refund(dto);
|
|
|
+
|
|
|
+ verify(merchantService).requireByTenantAndCode(TENANT, "M700");
|
|
|
+ verify(orderService).openRefund(dto);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 别租户的商户号必须在**发起退款之前**被挡下 —— 闸在后就等于拿着别人的单号去退款了 */
|
|
|
+ @Test
|
|
|
+ void refund_foreignMerchant_rejectedBeforeRefunding() {
|
|
|
+ when(merchantService.requireByTenantAndCode(TENANT, "M999"))
|
|
|
+ .thenThrow(new BusinessException(404, "商户编号不存在或不属于当前租户: M999"));
|
|
|
+ HarOpenRefundDTO dto = new HarOpenRefundDTO();
|
|
|
+ dto.setMerchantCode("M999");
|
|
|
+ dto.setOrderNo("HAR1");
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class, () -> controller.refund(dto));
|
|
|
+
|
|
|
+ assertEquals(404, e.getCode(), "实测消息: " + e.getMessage());
|
|
|
+ verifyNoInteractions(orderService);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 退款的**真实路径**。
|
|
|
+ *
|
|
|
+ * 路径写错在别处完全不可见:service 层用例直调 service,本类别的用例直调方法,
|
|
|
+ * 把 {@code @PostMapping("/refund")} 改成别的一行,那些用例照样全绿而线上 404。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void refund_isMappedAtTheDocumentedPath() throws Exception {
|
|
|
+ assertEquals("/refund", HarOpenController.class
|
|
|
+ .getDeclaredMethod("refund", HarOpenRefundDTO.class)
|
|
|
+ .getAnnotation(PostMapping.class).value()[0],
|
|
|
+ "租户文档 10.4 承诺的路径是 /payment/openapi/har/refund");
|
|
|
+ assertEquals("/payment/openapi/har",
|
|
|
+ HarOpenController.class.getAnnotation(RequestMapping.class).value()[0]);
|
|
|
+ }
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 本控制器每一个 {@code @PostMapping} 都要出现在
|
|
|
+ * {@code TenantApiKeyAuthFilter.OPENAPI_ACCOUNT_PATHS} 那份清单里。
|
|
|
+ *
|
|
|
+ * ★ 这条**不守鉴权边界**:认证判定读的是前缀数组,漏列**不会**造成无鉴权
|
|
|
+ * (该 filter 的 javadoc 明写了这一点)。它守的是那份自称「供文档与测试核对」的
|
|
|
+ * 清单与实际映射一致 —— 乐收赢有 LeshouyingWiringTest 在核对同一件事,
|
|
|
+ * 汇安融侧此前一条都没有,所以这条清单在 HAR 这边是**没人核对**的。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void everyEndpoint_isListedInTheAuthFilterInventory() throws Exception {
|
|
|
+ String base = HarOpenController.class.getAnnotation(RequestMapping.class).value()[0];
|
|
|
+ java.lang.reflect.Field f = com.payment.platform.core.security.TenantApiKeyAuthFilter.class
|
|
|
+ .getDeclaredField("OPENAPI_ACCOUNT_PATHS");
|
|
|
+ f.setAccessible(true);
|
|
|
+ java.util.List<String> listed =
|
|
|
+ java.util.Arrays.asList((String[]) f.get(null));
|
|
|
+
|
|
|
+ java.util.List<String> actual = new java.util.ArrayList<>();
|
|
|
+ for (var m : HarOpenController.class.getDeclaredMethods()) {
|
|
|
+ PostMapping pm = m.getAnnotation(PostMapping.class);
|
|
|
+ if (pm != null) {
|
|
|
+ actual.add(base + pm.value()[0]);
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ assertTrue(actual.size() >= 4, "一个端点都没扫到,说明这条用例的量具坏了: " + actual);
|
|
|
+ for (String path : actual) {
|
|
|
+ assertTrue(listed.contains(path), "未列入 OPENAPI_ACCOUNT_PATHS: " + path);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|