package com.yunhai.svchall.controller.api;
import com.yunhai.platform.apistd.spring.GlobalExceptionHandler;
import com.yunhai.platform.auth.BoaUserContext;
import com.yunhai.platform.auth.spring.BoaUserArgumentResolver;
import com.yunhai.platform.auth.spring.CasSsoApiRedirectFilter;
import com.yunhai.platform.utils.model.BoaUser;
import com.yunhai.svchall.entity.ServiceApply;
import com.yunhai.svchall.repository.ServiceApplyRepository;
import com.yunhai.svchall.service.ApplyViewGuard;
import com.yunhai.svchall.service.FlowClient;
import com.yunhai.svchall.service.impl.ServiceApplyService;
import com.yunhai.svchall.service.impl.SvcApprovalClient;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import java.util.Collections;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyMap;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
/**
* P3.1 审批代理三层测试之 MVC 契约层:MockMvc standalone 显式装配
* {@link BoaUserArgumentResolver}(身份只来自 {@link BoaUserContext})与
* {@link GlobalExceptionHandler}(R3.0 P1-04:范本 NoticeApprovalControllerTest 是方法单测,
* 证明不了解析器/advice 装配)。
*
* 断言口径分开:HTTP 状态恒 200(统一信封);未登录=业务码 3(FORBIDDEN);
* 无学校=业务码 2(PARAM);拒绝路径审批出站必须为 0(verifyNoInteractions)。
* 另有一组挂真实 {@link CasSsoApiRedirectFilter} 的过滤链用例:API 头未登录=HTTP200+code302
* (平台 CAS 口径,非 HTTP 403——R3.0 勘误)。
*/
@DisplayName("SvcApprovalController - MVC 契约(双闸/身份注入/信封映射)")
class SvcApprovalControllerTest {
private SvcApprovalClient approvalClient;
private ServiceApplyRepository applyRepository;
private ApplyViewGuard applyViewGuard;
private ServiceApplyService applyService;
private MockMvc mockMvc;
@BeforeEach
void setUp() {
approvalClient = mock(SvcApprovalClient.class);
applyRepository = mock(ServiceApplyRepository.class);
FlowClient flowClient = mock(FlowClient.class);
applyViewGuard = new ApplyViewGuard(flowClient);
applyService = mock(ServiceApplyService.class);
SvcApprovalController controller = new SvcApprovalController(
approvalClient, applyRepository, applyViewGuard, applyService);
mockMvc = MockMvcBuilders.standaloneSetup(controller)
.setCustomArgumentResolvers(new BoaUserArgumentResolver())
.setControllerAdvice(new GlobalExceptionHandler(null),
new com.yunhai.platform.apistd.spring.ApiResponseAdvice())
.build();
}
@AfterEach
void tearDown() {
BoaUserContext.clear();
}
/** 深桩用户(沿 ServiceApplyFlowSubmitTest 形态):roleIds 可指定以驱动 Guard 管理员分支 */
private BoaUser user(int id, int schId, Integer... roleIds) {
BoaUser user = mock(BoaUser.class, org.mockito.Mockito.RETURNS_DEEP_STUBS);
when(user.getId()).thenReturn(id);
when(user.getName()).thenReturn("张三");
when(user.getAccount()).thenReturn("zhangsan");
when(user.getCurrentRole().getSchId()).thenReturn(schId);
when(user.getRoleIds()).thenReturn(roleIds == null || roleIds.length == 0 ? java.util.Collections.emptyList() : java.util.Arrays.asList(roleIds));
when(user.getType()).thenReturn(null);
return user;
}
private ServiceApply apply(int id, int schId, Integer submitterId, Long instanceId) {
ServiceApply apply = new ServiceApply();
org.springframework.test.util.ReflectionTestUtils.setField(apply, "id", id);
apply.setSchId((long) schId);
apply.setSubmitterId(submitterId);
apply.setFlowInstanceId(instanceId);
return apply;
}
private static Map byBizTasks(Long instanceId, Map... tasks) {
Map m = new LinkedHashMap<>();
m.put("instanceId", instanceId);
m.put("tasks", java.util.Arrays.asList(tasks));
return m;
}
private static Map task(int taskId) {
Map t = new LinkedHashMap<>();
t.put("taskId", taskId);
t.put("nodeName", "部门负责人");
return t;
}
private static Map row(int id) {
Map m = new LinkedHashMap<>();
m.put("id", id);
return m;
}
private static Map pair(Object value) {
Map m = new LinkedHashMap<>();
m.put("total", 0L);
m.put("list", value == null ? new java.util.ArrayList<>() : Collections.singletonList(value));
return m;
}
/** RecordUrl 换址应答 {pageUrl} */
private static Map pageUrl(String url) {
Map m = new LinkedHashMap<>();
m.put("pageUrl", url);
return m;
}
// ===== 身份层 =====
@Test
@DisplayName("未登录(无 BoaUser):HTTP200 + 业务码 3,零出站")
void todoWithoutLogin() throws Exception {
mockMvc.perform(get("/api/apply/approval/Todo"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(3));
verifyNoInteractions(approvalClient);
}
@Test
@DisplayName("会话无学校归属:HTTP200 + 业务码 2,零出站")
void noSchoolIdentity() throws Exception {
BoaUser user = mock(BoaUser.class, org.mockito.Mockito.RETURNS_DEEP_STUBS);
when(user.getId()).thenReturn(10006);
when(user.getCurrentRole()).thenReturn(null);
BoaUserContext.set(user);
mockMvc.perform(get("/api/apply/approval/Todo"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
verifyNoInteractions(approvalClient);
}
@Test
@DisplayName("请求参数携带身份字段(operatorId/schId/…):一律拒绝,零出站")
void identityParamsRejected() throws Exception {
BoaUserContext.set(user(10006, 3));
mockMvc.perform(get("/api/apply/approval/Todo").param("operatorId", "1"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
mockMvc.perform(get("/api/apply/approval/ByBiz").param("applyId", "42").param("schId", "3"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
mockMvc.perform(post("/api/apply/approval/Approve")
.param("applyId", "42").param("taskId", "442").param("operatorName", "李四")
.contentType("application/json").content("{}"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.code").value(2));
verifyNoInteractions(approvalClient, applyRepository);
}
// ===== Todo / My =====
@Test
@DisplayName("Todo:会话身份出站(双值双查在 client),信封透传")
void todoHappyPath() throws Exception {
BoaUserContext.set(user(10006, 3));
Map data = new LinkedHashMap<>();
data.put("total", 0);
data.put("content", new java.util.ArrayList