Просмотр исходного кода

test(admin-auth): 微信扫码登录单测(SC-G-03)

覆盖:qr 返回 state/qrImage、poll 的 pending/expired/success/无后台角色拒绝/loginRole 指定、成功后 state 清除防重放
Sisyphus 1 неделя назад
Родитель
Сommit
a5691ffc9e

+ 249 - 0
cfc-backend/src/test/java/com/etotem/cfc/controller/admin/AdminAuthWechatScanTest.java

@@ -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);
+    }
+}