|
@@ -0,0 +1,372 @@
|
|
|
|
|
+package com.etotem.cfc.controller.admin;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.config.JwtConfig;
|
|
|
|
|
+import com.etotem.cfc.dto.AdminLoginDTO;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.LoginAttemptService;
|
|
|
|
|
+import com.etotem.cfc.service.UserService;
|
|
|
|
|
+import com.etotem.cfc.service.VerificationCodeService;
|
|
|
|
|
+import com.etotem.cfc.service.WechatWebLoginService;
|
|
|
|
|
+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.http.MediaType;
|
|
|
|
|
+import org.springframework.test.util.ReflectionTestUtils;
|
|
|
|
|
+import org.springframework.test.web.servlet.MockMvc;
|
|
|
|
|
+import org.springframework.test.web.servlet.setup.MockMvcBuilders;
|
|
|
|
|
+
|
|
|
|
|
+import javax.servlet.http.HttpServletRequest;
|
|
|
|
|
+import java.util.ArrayList;
|
|
|
|
|
+import java.util.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.List;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyList;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.anyString;
|
|
|
|
|
+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.when;
|
|
|
|
|
+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;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * S-cfc-17 / admin.auth.login 契约级单测(RED 阶段:补齐「未覆盖」能力键的最小闭环)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>与 {@link AdminAuthControllerLoginStatusTest}(B-2/R5:空 body/缺参 → 400 语义)互补,
|
|
|
|
|
+ * 本类聚焦 {@code POST /api/admin-auth/login} 的完整登录契约:
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>正常路径:admin / teacher / 遗留角色归一化(activity_admin→activity_manager,
|
|
|
|
|
+ * supplier_admin→supplier)/ 多角色 loginRole 身份选择 / 无 roles 列表回退主角色;</li>
|
|
|
|
|
+ * <li>关键边界:用户不存在、验证码错误(记录失败)、登录锁定、skipCaptcha 跳过校验、
|
|
|
|
|
+ * X-Forwarded-For IP 取数口径、nickname 为空回退手机号;</li>
|
|
|
|
|
+ * <li>角色与权限:parent/child 等 C 端账号拒绝、非法 loginRole 拒绝。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>契约口径(对齐项目惯例:控制器签名 {@code Result<T>} + HTTP 200 信封、业务码表达语义;
|
|
|
|
|
+ * 真实 HTTP 401 由 {@code JwtInterceptor} 对受保护路径负责,{@code /api/admin-auth/login}
|
|
|
|
|
+ * 为公开路径):
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>参数校验(空 body / 缺 phone / 缺 code)→ 业务码 400;</li>
|
|
|
|
|
+ * <li>业务拒绝(验证码错误 / 登录锁定 / 用户不存在 / 无后台角色 / 非法登录身份)
|
|
|
|
|
+ * → 业务码 500 + 明确 message —— 对应 blockers.md B-2 / test_blockers.md R5 期望的
|
|
|
|
|
+ * 401/403 语义,当前实现仍以业务码 500 表达(HTTP 200 信封),如实入测并登记缺陷,不改 src/main;</li>
|
|
|
|
|
+ * <li>成功 → 业务码 200 + token/adminId/role/roles。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ * 测试隔离:MockMvc standalone + 全 mock 依赖,无 Spring 上下文 / 无 DB / 无外部服务。
|
|
|
|
|
+ */
|
|
|
|
|
+class AdminAuthControllerLoginContractTest {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String PHONE = "13800000001";
|
|
|
|
|
+ private static final String CODE = "123456";
|
|
|
|
|
+
|
|
|
|
|
+ private AdminAuthController controller;
|
|
|
|
|
+ private MockMvc mockMvc;
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+ private UserService userService;
|
|
|
|
|
+ private VerificationCodeService verificationCodeService;
|
|
|
|
|
+ private JwtConfig jwtConfig;
|
|
|
|
|
+ private LoginAttemptService loginAttemptService;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() {
|
|
|
|
|
+ controller = new AdminAuthController();
|
|
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
|
|
+ userService = mock(UserService.class);
|
|
|
|
|
+ verificationCodeService = mock(VerificationCodeService.class);
|
|
|
|
|
+ jwtConfig = mock(JwtConfig.class);
|
|
|
|
|
+ loginAttemptService = mock(LoginAttemptService.class);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "userMapper", userMapper);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "userService", userService);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "verificationCodeService", verificationCodeService);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "jwtConfig", jwtConfig);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "loginAttemptService", loginAttemptService);
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "wechatWebLoginService", mock(WechatWebLoginService.class));
|
|
|
|
|
+ // @Value 默认 false;skip-captcha 路径由专门 case 打开
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "skipCaptcha", false);
|
|
|
|
|
+ mockMvc = MockMvcBuilders.standaloneSetup(controller).build();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+ // 正常路径
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:admin 验证码登录成功 → 200 + token/adminId/role/roles")
|
|
|
|
|
+ void login_admin_成功签发token与角色() throws Exception {
|
|
|
|
|
+ successStubs(user(100L, "admin", list("admin"), "nick100", "管理员100"), list("admin"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/admin-auth/login")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content("{\"phone\":\"" + PHONE + "\",\"code\":\"" + CODE + "\"}"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.token").value("jwt-token"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.adminId").value(100))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.username").value("nick100"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.realName").value("管理员100"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.role").value("admin"))
|
|
|
|
|
+ .andExpect(jsonPath("$.data.roles[0]").value("admin"));
|
|
|
|
|
+
|
|
|
|
|
+ verify(verificationCodeService).verifyCode(PHONE, CODE, "admin_login");
|
|
|
|
|
+ verify(loginAttemptService).isBlocked(PHONE + ":127.0.0.1");
|
|
|
|
|
+ verify(loginAttemptService).clearSuccess(PHONE + ":127.0.0.1");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:teacher 登录成功 → role=teacher(成长规划师可登录后台)")
|
|
|
|
|
+ void login_teacher_成功() {
|
|
|
|
|
+ Result<Map<String, Object>> r = loginSuccess(user(200L, "teacher", list("teacher"), "规划师2", null), list("teacher"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("teacher", r.getData().get("role"));
|
|
|
|
|
+ assertEquals("规划师2", r.getData().get("username"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:遗留角色 activity_admin 归一化为 activity_manager 登录成功")
|
|
|
|
|
+ void login_遗留角色_归一化为新角色() {
|
|
|
|
|
+ Result<Map<String, Object>> r = loginSuccess(user(300L, "activity_admin", list("activity_admin"), "旧活动", null), list("activity_admin"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("activity_manager", r.getData().get("role"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:遗留角色 supplier_admin 归一化为 supplier 登录成功")
|
|
|
|
|
+ void login_供应商遗留角色_归一化() {
|
|
|
|
|
+ Result<Map<String, Object>> r = loginSuccess(user(1100L, "supplier_admin", list("supplier_admin"), "旧供应商", null), list("supplier_admin"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("supplier", r.getData().get("role"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:无 roles 列表时回退主角色 role 字段(teacher)")
|
|
|
|
|
+ void login_无roles列表_回退主角色() {
|
|
|
|
|
+ User teacher = user(400L, "teacher", null, "回退", null);
|
|
|
|
|
+ successStubs(teacher, Collections.<String>emptyList());
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dto());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("teacher", r.getData().get("role"));
|
|
|
|
|
+ // getUserRoles 为空时回退为选中身份列表
|
|
|
|
|
+ assertEquals(list("teacher"), r.getData().get("roles"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:多角色 + loginRole 选择 → 以指定身份登录,token roles 选中身份置顶去重")
|
|
|
|
|
+ void login_多角色loginRole合法_选中身份置顶() {
|
|
|
|
|
+ User multi = user(500L, "admin", list("admin", "teacher"), "双角色", null);
|
|
|
|
|
+ successStubs(multi, list("admin", "teacher"));
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dtoWithLoginRole("teacher"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("teacher", r.getData().get("role"));
|
|
|
|
|
+ assertEquals(list("admin", "teacher"), r.getData().get("roles"));
|
|
|
|
|
+
|
|
|
|
|
+ ArgumentCaptor<List<String>> rolesCaptor = ArgumentCaptor.forClass(List.class);
|
|
|
|
|
+ verify(jwtConfig).generateToken(eq(500L), rolesCaptor.capture());
|
|
|
|
|
+ // selected=teacher 置顶,其余角色(去重后)随后
|
|
|
|
|
+ assertEquals(list("teacher", "admin"), rolesCaptor.getValue());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+ // 关键边界
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:用户不存在 → 业务拒绝,不签发 token 不清锁定")
|
|
|
|
|
+ void login_用户不存在_拒绝() {
|
|
|
|
|
+ when(loginAttemptService.isBlocked(anyString())).thenReturn(false);
|
|
|
|
|
+ when(verificationCodeService.verifyCode(PHONE, CODE, "admin_login")).thenReturn(true);
|
|
|
|
|
+ when(userMapper.selectOne(any())).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dto());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("用户不存在", r.getMessage());
|
|
|
|
|
+ verify(loginAttemptService, never()).clearSuccess(anyString());
|
|
|
|
|
+ verify(jwtConfig, never()).generateToken(any(), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:验证码错误 → 拒绝并记录失败尝试")
|
|
|
|
|
+ void login_验证码错误_记录失败() {
|
|
|
|
|
+ when(loginAttemptService.isBlocked(anyString())).thenReturn(false);
|
|
|
|
|
+ when(verificationCodeService.verifyCode(PHONE, CODE, "admin_login")).thenReturn(false);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dto());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("验证码错误或已过期", r.getMessage());
|
|
|
|
|
+ verify(loginAttemptService).recordFailure(anyString());
|
|
|
|
|
+ verify(userMapper, never()).selectOne(any());
|
|
|
|
|
+ verify(jwtConfig, never()).generateToken(any(), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:登录锁定 → 拒绝且不触达验证码与用户查询(含剩余分钟提示)")
|
|
|
|
|
+ void login_登录锁定_拒绝() {
|
|
|
|
|
+ when(loginAttemptService.isBlocked(anyString())).thenReturn(true);
|
|
|
|
|
+ // 剩余 900s=15min → 提示「15+1」分钟
|
|
|
|
|
+ when(loginAttemptService.getLockRemainingTime(anyString())).thenReturn(900_000L);
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dto());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("登录尝试过多,请16分钟后重试", r.getMessage());
|
|
|
|
|
+ verify(verificationCodeService, never()).verifyCode(anyString(), anyString(), anyString());
|
|
|
|
|
+ verify(userMapper, never()).selectOne(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:skipCaptcha=true 时跳过验证码校验直接登录")
|
|
|
|
|
+ void login_skipCaptcha_跳过验证码() throws Exception {
|
|
|
|
|
+ ReflectionTestUtils.setField(controller, "skipCaptcha", true);
|
|
|
|
|
+ successStubs(user(600L, "admin", list("admin"), "免码", null), list("admin"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/admin-auth/login")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content("{\"phone\":\"" + PHONE + "\",\"code\":\"wrong-code\"}"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200));
|
|
|
|
|
+
|
|
|
|
|
+ verify(verificationCodeService, never()).verifyCode(anyString(), anyString(), anyString());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:X-Forwarded-For 作为锁键 IP 口径(防绕过锁定)")
|
|
|
|
|
+ void login_IP取数_优先XForwardedFor() throws Exception {
|
|
|
|
|
+ successStubs(user(700L, "admin", list("admin"), "ip", null), list("admin"));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/admin-auth/login")
|
|
|
|
|
+ .header("X-Forwarded-For", "203.0.113.5")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content("{\"phone\":\"" + PHONE + "\",\"code\":\"" + CODE + "\"}"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(200));
|
|
|
|
|
+
|
|
|
|
|
+ verify(loginAttemptService).isBlocked(PHONE + ":203.0.113.5");
|
|
|
|
|
+ verify(loginAttemptService).clearSuccess(PHONE + ":203.0.113.5");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:nickname 为空时 username 回退手机号,realName 保留")
|
|
|
|
|
+ void login_nickname为空_回退手机号() {
|
|
|
|
|
+ Result<Map<String, Object>> r = loginSuccess(user(800L, "admin", list("admin"), null, "实名张"), list("admin"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals(PHONE, r.getData().get("username"));
|
|
|
|
|
+ assertEquals("实名张", r.getData().get("realName"));
|
|
|
|
|
+ assertEquals(800L, r.getData().get("adminId"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+ // 角色 / 权限
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:parent 无后台角色 → 拒绝(该账号没有管理权限),HTTP 200 信封 + 业务码表达")
|
|
|
|
|
+ void login_parent无后台角色_拒绝() throws Exception {
|
|
|
|
|
+ when(loginAttemptService.isBlocked(anyString())).thenReturn(false);
|
|
|
|
|
+ when(verificationCodeService.verifyCode(PHONE, CODE, "admin_login")).thenReturn(true);
|
|
|
|
|
+ when(userMapper.selectOne(any())).thenReturn(user(900L, "parent", list("parent"), "家长", null));
|
|
|
|
|
+
|
|
|
|
|
+ mockMvc.perform(post("/api/admin-auth/login")
|
|
|
|
|
+ .contentType(MediaType.APPLICATION_JSON)
|
|
|
|
|
+ .content("{\"phone\":\"" + PHONE + "\",\"code\":\"" + CODE + "\"}"))
|
|
|
|
|
+ .andExpect(status().isOk())
|
|
|
|
|
+ .andExpect(jsonPath("$.code").value(500))
|
|
|
|
|
+ .andExpect(jsonPath("$.message").value("该账号没有管理权限"));
|
|
|
|
|
+
|
|
|
|
|
+ verify(jwtConfig, never()).generateToken(any(), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:roles 仅含 child(C 端)→ 拒绝")
|
|
|
|
|
+ void login_child角色_拒绝() {
|
|
|
|
|
+ when(loginAttemptService.isBlocked(anyString())).thenReturn(false);
|
|
|
|
|
+ when(verificationCodeService.verifyCode(PHONE, CODE, "admin_login")).thenReturn(true);
|
|
|
|
|
+ when(userMapper.selectOne(any())).thenReturn(user(901L, "child", list("child"), "孩子", null));
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dto());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("该账号没有管理权限", r.getMessage());
|
|
|
|
|
+ verify(jwtConfig, never()).generateToken(any(), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("login:登录身份不在账号后台角色内 → 拒绝(该账号没有【X】身份)")
|
|
|
|
|
+ void login_loginRole非法_拒绝() {
|
|
|
|
|
+ successStubs(user(1000L, "admin", list("admin"), "仅管理", null), list("admin"));
|
|
|
|
|
+
|
|
|
|
|
+ Result<Map<String, Object>> r = loginDirect(dtoWithLoginRole("finance"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("该账号没有【财务】身份", r.getMessage());
|
|
|
|
|
+ verify(jwtConfig, never()).generateToken(any(), anyList());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+ // helpers
|
|
|
|
|
+ // ================================================================
|
|
|
|
|
+
|
|
|
|
|
+ /** 成功路径标准打桩:未锁定、验证码通过、用户命中、角色列表、token 固定值 */
|
|
|
|
|
+ private void successStubs(User user, List<String> roles) {
|
|
|
|
|
+ when(loginAttemptService.isBlocked(anyString())).thenReturn(false);
|
|
|
|
|
+ when(verificationCodeService.verifyCode(PHONE, CODE, "admin_login")).thenReturn(true);
|
|
|
|
|
+ when(userMapper.selectOne(any())).thenReturn(user);
|
|
|
|
|
+ when(userService.getUserRoles(user.getId())).thenReturn(roles);
|
|
|
|
|
+ when(jwtConfig.generateToken(eq(user.getId()), anyList())).thenReturn("jwt-token");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 成功路径便捷入口:打桩 + 直接调用 login */
|
|
|
|
|
+ private Result<Map<String, Object>> loginSuccess(User user, List<String> roles) {
|
|
|
|
|
+ successStubs(user, roles);
|
|
|
|
|
+ return loginDirect(dto());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 直接调用 login(调用方自行打桩;request 为全 mock → 无转发头、remoteAddr 为 null) */
|
|
|
|
|
+ private Result<Map<String, Object>> loginDirect(AdminLoginDTO dto) {
|
|
|
|
|
+ return controller.login(dto, mock(HttpServletRequest.class));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AdminLoginDTO dto() {
|
|
|
|
|
+ return dtoWithLoginRole(null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private AdminLoginDTO dtoWithLoginRole(String loginRole) {
|
|
|
|
|
+ AdminLoginDTO dto = new AdminLoginDTO();
|
|
|
|
|
+ dto.setPhone(PHONE);
|
|
|
|
|
+ dto.setCode(CODE);
|
|
|
|
|
+ dto.setLoginRole(loginRole);
|
|
|
|
|
+ return dto;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static User user(Long id, String role, List<String> roles, String nickname, String realName) {
|
|
|
|
|
+ User user = new User();
|
|
|
|
|
+ user.setId(id);
|
|
|
|
|
+ user.setRole(role);
|
|
|
|
|
+ user.setRoles(roles);
|
|
|
|
|
+ user.setNickname(nickname);
|
|
|
|
|
+ user.setRealName(realName);
|
|
|
|
|
+ user.setPhone(PHONE);
|
|
|
|
|
+ return user;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private static List<String> list(String... items) {
|
|
|
|
|
+ return new ArrayList<>(Arrays.asList(items));
|
|
|
|
|
+ }
|
|
|
|
|
+}
|