|
|
@@ -3,7 +3,9 @@ package com.payment.platform.module.payment.leshouying.controller;
|
|
|
import com.baomidou.mybatisplus.core.MybatisConfiguration;
|
|
|
import com.baomidou.mybatisplus.core.conditions.AbstractWrapper;
|
|
|
import com.baomidou.mybatisplus.core.metadata.TableInfoHelper;
|
|
|
+import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
|
|
import com.payment.platform.common.exception.BusinessException;
|
|
|
+import com.payment.platform.core.security.LoginUser;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyNotifyLogEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.entity.LsyOrderEntity;
|
|
|
import com.payment.platform.module.payment.leshouying.dto.LsyOrderRefundDTO;
|
|
|
@@ -15,6 +17,9 @@ import com.payment.platform.module.payment.leshouying.service.LsyRefundService;
|
|
|
import org.apache.ibatis.builder.MapperBuilderAssistant;
|
|
|
import org.mockito.ArgumentMatchers;
|
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
|
|
+import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
|
|
+import org.springframework.security.core.context.SecurityContextHolder;
|
|
|
+import org.junit.jupiter.api.AfterEach;
|
|
|
import org.junit.jupiter.api.BeforeEach;
|
|
|
import org.junit.jupiter.api.Test;
|
|
|
import org.junit.jupiter.api.extension.ExtendWith;
|
|
|
@@ -69,8 +74,41 @@ class LeshouyingOrderControllerTest {
|
|
|
TableInfoHelper.initTableInfo(new MapperBuilderAssistant(configuration, ""), LsyNotifyLogEntity.class);
|
|
|
}
|
|
|
|
|
|
+ /**
|
|
|
+ * ★ 必须清:{@code SecurityContextHolder} 是**线程本地且跨用例存活**的,
|
|
|
+ * 而 Surefire 默认在同一线程里跑完整个类。不清的话,某条用例的
|
|
|
+ * {@code login(null, true)} 会泄漏给后面每一条「不该放行」的用例,
|
|
|
+ * 让它们集体变绿 —— 这正是那种「全绿但什么也没验」的形态。
|
|
|
+ */
|
|
|
+ @AfterEach
|
|
|
+ void clearSecurityContext() {
|
|
|
+ SecurityContextHolder.clearContext();
|
|
|
+ }
|
|
|
+
|
|
|
// ────────────────────────────── 工具 ──────────────────────────────
|
|
|
|
|
|
+ /** 把当前主体放进 SecurityContext(照 {@code LeshouyingOnboardingControllerTest#login})。 */
|
|
|
+ private static void login(Long tenantId, boolean isSuperuser) {
|
|
|
+ LoginUser u = new LoginUser();
|
|
|
+ u.setUserId(1L);
|
|
|
+ u.setUsername("tester");
|
|
|
+ u.setStatus("0");
|
|
|
+ u.setTenantId(tenantId);
|
|
|
+ u.setIsSuperuser(isSuperuser);
|
|
|
+ u.setAuthorities(List.of());
|
|
|
+ SecurityContextHolder.getContext().setAuthentication(
|
|
|
+ new UsernamePasswordAuthenticationToken(u, null, List.of()));
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 取出 {@code /page} 实际用掉的 wrapper。 */
|
|
|
+ @SuppressWarnings("unchecked")
|
|
|
+ private AbstractWrapper<?, ?, ?> capturedPageQuery() {
|
|
|
+ ArgumentCaptor<com.baomidou.mybatisplus.core.conditions.Wrapper<LsyOrderEntity>> c =
|
|
|
+ ArgumentCaptor.forClass(com.baomidou.mybatisplus.core.conditions.Wrapper.class);
|
|
|
+ verify(orderMapper).selectPage(any(), c.capture());
|
|
|
+ return (AbstractWrapper<?, ?, ?>) c.getValue();
|
|
|
+ }
|
|
|
+
|
|
|
private LsyOrderEntity order() {
|
|
|
LsyOrderEntity o = new LsyOrderEntity();
|
|
|
o.setId(1L);
|
|
|
@@ -289,4 +327,66 @@ class LeshouyingOrderControllerTest {
|
|
|
"退款必须有自己的权限串。复用 order:list 的后果不是「少一层保护」而是**放大权限**:"
|
|
|
+ "任何能查一笔单的人都能把这笔钱退出去");
|
|
|
}
|
|
|
+
|
|
|
+ // ───────────────────── 超管按租户筛选(2026-09-30) ─────────────────────
|
|
|
+
|
|
|
+ /**
|
|
|
+ * 这四条是一组:①②是**负/正对照**(同一个量具读同一个 wrapper),③④是**闸**。
|
|
|
+ *
|
|
|
+ * <p>【为什么非超管传 tenantId 要 403,而不是静默忽略】
|
|
|
+ * {@code TenantInnerInterceptor} 对非超管会自动追加 {@code tenant_id = 自己的}。
|
|
|
+ * 若这里忽略他传的值,调用方请求的是 5 号租户、拿到的却是**自己租户的订单**,
|
|
|
+ * 而 HTTP 是 200 —— 界面会把这份列表当成「5 号租户的订单」显示。
|
|
|
+ * 这比返回空列表更坏:它是**一个看起来正确的错误答案**。
|
|
|
+ * 同一条纪律见 {@code LeshouyingOnboardingController} 对请求体里 {@code tenant_id} 的处理。
|
|
|
+ */
|
|
|
+ @Test
|
|
|
+ void page_不传租户_不加租户谓词_由拦截器兜底() {
|
|
|
+ when(orderMapper.selectPage(any(), any())).thenReturn(new Page<>(1, 20));
|
|
|
+
|
|
|
+ controller.page(1, 20, null, null, null, null);
|
|
|
+
|
|
|
+ String seg = capturedPageQuery().getSqlSegment();
|
|
|
+ assertFalse(seg.contains("tenant_id"),
|
|
|
+ "不传 tenantId 时本方法**不得**自己拼租户条件:非超管的隔离由 "
|
|
|
+ + "TenantInnerInterceptor 追加,两处都写会让谓词互相打架。实测: " + seg);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void page_超管传租户_按该租户过滤() {
|
|
|
+ login(null, true); // 超管自己可以没有归属租户 —— 这恰是它能跨租户看的原因
|
|
|
+ when(orderMapper.selectPage(any(), any())).thenReturn(new Page<>(1, 20));
|
|
|
+
|
|
|
+ controller.page(1, 20, null, null, null, 5L);
|
|
|
+
|
|
|
+ AbstractWrapper<?, ?, ?> w = capturedPageQuery();
|
|
|
+ assertTrue(w.getSqlSegment().contains("tenant_id"),
|
|
|
+ "超管传了 tenantId 就必须落到 WHERE 上;实测: " + w.getSqlSegment());
|
|
|
+ assertEquals(List.of(5L), boundValues(w),
|
|
|
+ "筛的必须是**那一个**租户,且是等值(不是 like)");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void page_非超管传租户_403_且一条库都不查() {
|
|
|
+ login(3L, false);
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> controller.page(1, 20, null, null, null, 5L));
|
|
|
+
|
|
|
+ assertEquals(403, e.getCode(),
|
|
|
+ "非超管按租户筛选 = 越权探测;忽略它会给出一份**看起来正确**的错列表");
|
|
|
+ verify(orderMapper, never()).selectPage(any(), any());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ void page_无登录主体时传租户_也按非超管拒() {
|
|
|
+ SecurityContextHolder.clearContext();
|
|
|
+
|
|
|
+ BusinessException e = assertThrows(BusinessException.class,
|
|
|
+ () -> controller.page(1, 20, null, null, null, 5L));
|
|
|
+
|
|
|
+ assertEquals(403, e.getCode(),
|
|
|
+ "取不到主体时**不能默认放行**(写成 Boolean.FALSE.equals 就会在这里放行)");
|
|
|
+ verify(orderMapper, never()).selectPage(any(), any());
|
|
|
+ }
|
|
|
}
|