|
|
@@ -0,0 +1,249 @@
|
|
|
+package com.etotem.cfc.controller.admin;
|
|
|
+
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
+import com.etotem.cfc.config.JwtConfig;
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
+import com.etotem.cfc.service.UserService;
|
|
|
+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 java.lang.reflect.Field;
|
|
|
+import java.util.ArrayList;
|
|
|
+import java.util.Arrays;
|
|
|
+import java.util.Collections;
|
|
|
+import java.util.HashMap;
|
|
|
+import java.util.List;
|
|
|
+import java.util.Map;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertEquals;
|
|
|
+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.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;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 后台管理端微信扫码登录(复用小程序码扫码 SSO 的 state 机制)。
|
|
|
+ *
|
|
|
+ * <p>覆盖:二维码生成、待扫码/过期轮询、成功后签发管理端 JWT 并清除 state(防重放)、
|
|
|
+ * 无后台角色(parent/child)拒绝、身份不匹配拒绝。</p>
|
|
|
+ */
|
|
|
+class AdminAuthWechatScanTest {
|
|
|
+
|
|
|
+ private AdminAuthController controller;
|
|
|
+ private WechatWebLoginService wechatWebLoginService;
|
|
|
+ private UserMapper userMapper;
|
|
|
+ private UserService userService;
|
|
|
+ private JwtConfig jwtConfig;
|
|
|
+
|
|
|
+ @BeforeEach
|
|
|
+ void setUp() throws Exception {
|
|
|
+ controller = new AdminAuthController();
|
|
|
+ wechatWebLoginService = mock(WechatWebLoginService.class);
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
+ userService = mock(UserService.class);
|
|
|
+ jwtConfig = mock(JwtConfig.class);
|
|
|
+ inject("wechatWebLoginService", wechatWebLoginService);
|
|
|
+ inject("userMapper", userMapper);
|
|
|
+ inject("userService", userService);
|
|
|
+ inject("jwtConfig", jwtConfig);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-qr:返回非空 state,test-mode 下 qrImage 为空串")
|
|
|
+ void scanQr_returnsStateAndQrImage() {
|
|
|
+ when(wechatWebLoginService.generateQrImage(anyString())).thenReturn("");
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanQr();
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ assertNotNull(result.getData());
|
|
|
+ Object state = result.getData().get("state");
|
|
|
+ assertNotNull(state);
|
|
|
+ assertTrue(state.toString().length() > 0);
|
|
|
+ assertEquals("", result.getData().get("qrImage"));
|
|
|
+ verify(wechatWebLoginService).putState(state.toString(), "P");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-qr:generateQrImage 异常时清 state 并返回错误")
|
|
|
+ void scanQr_qrImageFailure_removesStateAndErrors() {
|
|
|
+ when(wechatWebLoginService.generateQrImage(anyString()))
|
|
|
+ .thenThrow(new RuntimeException("微信接口不可用"));
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanQr();
|
|
|
+
|
|
|
+ assertEquals(500, result.getCode());
|
|
|
+ assertTrue(result.getMessage().contains("微信接口不可用"));
|
|
|
+ verify(wechatWebLoginService).removeState(anyString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:state=null 返回 expired")
|
|
|
+ void poll_stateMissing_returnsExpired() {
|
|
|
+ when(wechatWebLoginService.getState("s1")).thenReturn(null);
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body("s1"));
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ assertEquals("expired", result.getData().get("status"));
|
|
|
+ verify(wechatWebLoginService, never()).removeState(anyString());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:state=P 返回 pending")
|
|
|
+ void poll_pending_returnsPending() {
|
|
|
+ when(wechatWebLoginService.getState("s2")).thenReturn("P");
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body("s2"));
|
|
|
+
|
|
|
+ assertEquals("pending", result.getData().get("status"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:有后台角色 → success,返回 token/adminId/role/roles")
|
|
|
+ void poll_backendRole_returnsSuccessWithToken() {
|
|
|
+ User user = user(9L, "admin", Collections.singletonList("admin"));
|
|
|
+ when(wechatWebLoginService.getState("s3")).thenReturn("U9");
|
|
|
+ when(userMapper.selectById(9L)).thenReturn(user);
|
|
|
+ when(userService.getUserRoles(9L)).thenReturn(Collections.singletonList("admin"));
|
|
|
+ when(jwtConfig.generateToken(eq(9L), anyList())).thenReturn("jwt-token");
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body("s3"));
|
|
|
+
|
|
|
+ assertEquals(200, result.getCode());
|
|
|
+ Map<String, Object> data = result.getData();
|
|
|
+ assertEquals("success", data.get("status"));
|
|
|
+ assertEquals("jwt-token", data.get("token"));
|
|
|
+ assertEquals(9L, data.get("adminId"));
|
|
|
+ assertEquals("admin", data.get("role"));
|
|
|
+ assertEquals(Collections.singletonList("admin"), data.get("roles"));
|
|
|
+ assertEquals("管理员9", data.get("realName"));
|
|
|
+ // 成功后 state 清除,防重放
|
|
|
+ verify(wechatWebLoginService).removeState("s3");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:parent 无后台角色 → error 且不签发 token")
|
|
|
+ void poll_clientRole_returnsError() {
|
|
|
+ User user = user(18L, "parent", Collections.singletonList("parent"));
|
|
|
+ when(wechatWebLoginService.getState("s4")).thenReturn("U18");
|
|
|
+ when(userMapper.selectById(18L)).thenReturn(user);
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body("s4"));
|
|
|
+
|
|
|
+ Map<String, Object> data = result.getData();
|
|
|
+ assertEquals("error", data.get("status"));
|
|
|
+ assertEquals("该账号没有管理权限", data.get("message"));
|
|
|
+ assertNull(data.get("token"));
|
|
|
+ verify(jwtConfig, never()).generateToken(any(Long.class), anyList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:child 无后台角色(roles 为空)→ error")
|
|
|
+ void poll_childRole_returnsError() {
|
|
|
+ User user = user(19L, "child", null);
|
|
|
+ when(wechatWebLoginService.getState("s5")).thenReturn("U19");
|
|
|
+ when(userMapper.selectById(19L)).thenReturn(user);
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body("s5"));
|
|
|
+
|
|
|
+ assertEquals("error", result.getData().get("status"));
|
|
|
+ assertEquals("该账号没有管理权限", result.getData().get("message"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:指定 loginRole 命中多角色 → token role 为选中身份")
|
|
|
+ void poll_multiRoleWithSelectedRole_usesSelected() {
|
|
|
+ User user = user(20L, "teacher", Arrays.asList("teacher", "org_admin"));
|
|
|
+ when(wechatWebLoginService.getState("s6")).thenReturn("U20");
|
|
|
+ when(userMapper.selectById(20L)).thenReturn(user);
|
|
|
+ when(userService.getUserRoles(20L)).thenReturn(Arrays.asList("teacher", "org_admin"));
|
|
|
+ when(jwtConfig.generateToken(eq(20L), anyList())).thenReturn("jwt-org");
|
|
|
+
|
|
|
+ Map<String, String> body = body("s6");
|
|
|
+ body.put("loginRole", "org_admin");
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body);
|
|
|
+
|
|
|
+ assertEquals("success", result.getData().get("status"));
|
|
|
+ assertEquals("org_admin", result.getData().get("role"));
|
|
|
+ assertEquals("jwt-org", result.getData().get("token"));
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:指定 loginRole 不在账号角色内 → error")
|
|
|
+ void poll_selectedRoleNotOwned_returnsError() {
|
|
|
+ User user = user(21L, "teacher", Collections.singletonList("teacher"));
|
|
|
+ when(wechatWebLoginService.getState("s7")).thenReturn("U21");
|
|
|
+ when(userMapper.selectById(21L)).thenReturn(user);
|
|
|
+
|
|
|
+ Map<String, String> body = body("s7");
|
|
|
+ body.put("loginRole", "admin");
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body);
|
|
|
+
|
|
|
+ assertEquals("error", result.getData().get("status"));
|
|
|
+ assertEquals("该账号没有【超级管理员】身份", result.getData().get("message"));
|
|
|
+ verify(jwtConfig, never()).generateToken(any(Long.class), anyList());
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:成功后再次轮询同 state → expired(防重放)")
|
|
|
+ void poll_afterSuccess_stateCleared() {
|
|
|
+ User user = user(22L, "admin", Collections.singletonList("admin"));
|
|
|
+ when(wechatWebLoginService.getState("s8")).thenReturn("U22").thenReturn(null);
|
|
|
+ when(userMapper.selectById(22L)).thenReturn(user);
|
|
|
+ when(userService.getUserRoles(22L)).thenReturn(Collections.singletonList("admin"));
|
|
|
+ when(jwtConfig.generateToken(eq(22L), anyList())).thenReturn("jwt-2");
|
|
|
+
|
|
|
+ Result<Map<String, Object>> first = controller.wechatScanPoll(body("s8"));
|
|
|
+ Result<Map<String, Object>> second = controller.wechatScanPoll(body("s8"));
|
|
|
+
|
|
|
+ assertEquals("success", first.getData().get("status"));
|
|
|
+ assertEquals("expired", second.getData().get("status"));
|
|
|
+ verify(wechatWebLoginService).removeState("s8");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("wechat-scan-poll:缺少 state 返回错误提示")
|
|
|
+ void poll_blankState_returnsError() {
|
|
|
+ Map<String, String> body = new HashMap<>();
|
|
|
+ body.put("state", " ");
|
|
|
+
|
|
|
+ Result<Map<String, Object>> result = controller.wechatScanPoll(body);
|
|
|
+
|
|
|
+ assertEquals(500, result.getCode());
|
|
|
+ assertEquals("缺少 state", result.getMessage());
|
|
|
+ }
|
|
|
+
|
|
|
+ private static Map<String, String> body(String state) {
|
|
|
+ Map<String, String> body = new HashMap<>();
|
|
|
+ body.put("state", state);
|
|
|
+ return body;
|
|
|
+ }
|
|
|
+
|
|
|
+ private static User user(Long id, String role, List<String> roles) {
|
|
|
+ User user = new User();
|
|
|
+ user.setId(id);
|
|
|
+ user.setRole(role);
|
|
|
+ user.setRoles(roles == null ? null : new ArrayList<>(roles));
|
|
|
+ user.setNickname("nick" + id);
|
|
|
+ user.setRealName("管理员" + id);
|
|
|
+ user.setPhone("1380000" + id);
|
|
|
+ return user;
|
|
|
+ }
|
|
|
+
|
|
|
+ private void inject(String fieldName, Object value) throws Exception {
|
|
|
+ Field field = AdminAuthController.class.getDeclaredField(fieldName);
|
|
|
+ field.setAccessible(true);
|
|
|
+ field.set(controller, value);
|
|
|
+ }
|
|
|
+}
|