|
@@ -0,0 +1,256 @@
|
|
|
|
|
+package com.etotem.cfc.unit;
|
|
|
|
|
+
|
|
|
|
|
+import com.etotem.cfc.common.Result;
|
|
|
|
|
+import com.etotem.cfc.controller.family.FamilyUserController;
|
|
|
|
|
+import com.etotem.cfc.entity.User;
|
|
|
|
|
+import com.etotem.cfc.mapper.UserMapper;
|
|
|
|
|
+import com.etotem.cfc.service.UserService;
|
|
|
|
|
+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.Arrays;
|
|
|
|
|
+import java.util.Collections;
|
|
|
|
|
+import java.util.HashMap;
|
|
|
|
|
+import java.util.Map;
|
|
|
|
|
+
|
|
|
|
|
+import static org.junit.jupiter.api.Assertions.*;
|
|
|
|
|
+import static org.mockito.ArgumentMatchers.any;
|
|
|
|
|
+import static org.mockito.Mockito.*;
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * S-cfc-11 / family.parent.switchRole 契约级单测(RED 阶段补覆盖)。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>覆盖 POST /api/family/user/switch-role(FamilyUserController.switchRole):
|
|
|
|
|
+ * <ul>
|
|
|
|
|
+ * <li>正常路径:家长拥有 teacher 角色时切换到 teacher;child 持有 parent 角色可切回家长;</li>
|
|
|
|
|
+ * <li>关键边界:未登录(userId null / ≤0)→ 401;服务层返回 null → 业务错误;role 参数缺失;</li>
|
|
|
|
|
+ * <li>角色/权限:admin 当前角色不可切换、未持有目标角色不可切换、非法目标角色拒绝。</li>
|
|
|
|
|
+ * </ul>
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>纯单测(JUnit5 + Mockito 直接调用),不依赖 Spring 上下文 / 微信 / 外部服务。
|
|
|
|
|
+ * 仅覆盖 cfc-backend 已有实现,不改 src/main。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>注意:不使用 {@code @Nested}(surefire 2.22.2 对仅嵌套测试的类按精确类名
|
|
|
|
|
+ * {@code -Dtest=FQN} 过滤会报 "No tests were executed"),直接平铺便于
|
|
|
|
|
+ * {@code mvn -DskipTests=false -Dtest=FamilyUserSwitchRoleTest test} 精确执行。
|
|
|
|
|
+ */
|
|
|
|
|
+@DisplayName("family.parent.switchRole 契约(FamilyUserController.switchRole)")
|
|
|
|
|
+class FamilyUserSwitchRoleTest {
|
|
|
|
|
+
|
|
|
|
|
+ // ========================================================================
|
|
|
|
|
+ // Controller 契约层:直接调用 FamilyUserController.switchRole,mock UserService
|
|
|
|
|
+ // ========================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private FamilyUserController controller;
|
|
|
|
|
+ private UserService userService;
|
|
|
|
|
+
|
|
|
|
|
+ // ========================================================================
|
|
|
|
|
+ // Service 角色/权限层:直接调用 UserService.switchRole,mock UserMapper
|
|
|
|
|
+ // ========================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private UserService service;
|
|
|
|
|
+ private UserMapper userMapper;
|
|
|
|
|
+
|
|
|
|
|
+ @BeforeEach
|
|
|
|
|
+ void setUp() throws Exception {
|
|
|
|
|
+ // Controller 层
|
|
|
|
|
+ controller = new FamilyUserController();
|
|
|
|
|
+ userService = mock(UserService.class);
|
|
|
|
|
+ setField(controller, "userService", userService);
|
|
|
|
|
+ // Service 层
|
|
|
|
|
+ service = new UserService();
|
|
|
|
|
+ userMapper = mock(UserMapper.class);
|
|
|
|
|
+ setField(service, "userMapper", userMapper);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+ // Controller:正常路径
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("成功切换:家长拥有 teacher 角色 → 返回目标角色")
|
|
|
|
|
+ void controller_成功切换_返回目标角色() {
|
|
|
|
|
+ when(userService.switchRole(1L, "teacher")).thenReturn("teacher");
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> r = controller.switchRole(1L, body("teacher"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(200, r.getCode());
|
|
|
|
|
+ assertEquals("teacher", r.getData());
|
|
|
|
|
+ verify(userService).switchRole(1L, "teacher");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+ // Controller:关键边界(未登录 / role 缺失 / 服务层空结果)
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("未登录(userId=null)→ 401,不触达服务层")
|
|
|
|
|
+ void controller_未登录_userId为null_返回401() {
|
|
|
|
|
+ Result<String> r = controller.switchRole(null, body("teacher"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(401, r.getCode());
|
|
|
|
|
+ assertEquals("请先登录", r.getMessage());
|
|
|
|
|
+ verify(userService, never()).switchRole(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("未登录(userId=-1 / 0 匿名)→ 401,不触达服务层")
|
|
|
|
|
+ void controller_未登录_userId非正_返回401() {
|
|
|
|
|
+ Result<String> r1 = controller.switchRole(-1L, body("teacher"));
|
|
|
|
|
+ assertEquals(401, r1.getCode());
|
|
|
|
|
+ assertEquals("请先登录", r1.getMessage());
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> r2 = controller.switchRole(0L, body("teacher"));
|
|
|
|
|
+ assertEquals(401, r2.getCode());
|
|
|
|
|
+
|
|
|
|
|
+ verify(userService, never()).switchRole(any(), any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("role 参数缺失 → 透传服务层空结果,提示切换失败")
|
|
|
|
|
+ void controller_缺失role参数_提示切换失败() {
|
|
|
|
|
+ Result<String> r = controller.switchRole(1L, Collections.emptyMap());
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("切换失败,请确认您拥有该角色", r.getMessage());
|
|
|
|
|
+ verify(userService).switchRole(1L, null);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("服务层返回 null(非法角色/用户不存在)→ 业务错误提示")
|
|
|
|
|
+ void controller_服务返回null_提示切换失败() {
|
|
|
|
|
+ when(userService.switchRole(1L, "hacker")).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ Result<String> r = controller.switchRole(1L, body("hacker"));
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals(500, r.getCode());
|
|
|
|
|
+ assertEquals("切换失败,请确认您拥有该角色", r.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+ // Controller:角色/权限异常传播(不得吞成 200)
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("越权:目标角色不属于用户 → RuntimeException 向上传播,不得吞成 200")
|
|
|
|
|
+ void controller_无目标角色权限_异常传播() {
|
|
|
|
|
+ when(userService.switchRole(1L, "teacher"))
|
|
|
|
|
+ .thenThrow(new RuntimeException("您没有该角色权限"));
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> controller.switchRole(1L, body("teacher")));
|
|
|
|
|
+ assertEquals("您没有该角色权限", ex.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("越权:当前角色为 admin → RuntimeException 向上传播,不得吞成 200")
|
|
|
|
|
+ void controller_管理员不允许切换_异常传播() {
|
|
|
|
|
+ when(userService.switchRole(1L, "parent"))
|
|
|
|
|
+ .thenThrow(new RuntimeException("管理员角色不允许切换"));
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> controller.switchRole(1L, body("parent")));
|
|
|
|
|
+ assertEquals("管理员角色不允许切换", ex.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+ // Service:正常路径 / 角色权限
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常路径:家长持有 teacher → 更新 role 并返回目标角色")
|
|
|
|
|
+ void service_拥有目标角色_更新并返回() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "parent", "parent", "teacher"));
|
|
|
|
|
+
|
|
|
|
|
+ String out = service.switchRole(1L, "teacher");
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("teacher", out);
|
|
|
|
|
+ verify(userMapper).updateById(argThat(u -> "teacher".equals(u.getRole())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("正常路径:child 持有 parent 角色时切换回家长(角色白名单内)")
|
|
|
|
|
+ void service_child持有parent角色_可切回家长() {
|
|
|
|
|
+ when(userMapper.selectById(2L)).thenReturn(user(2L, "child", "parent", "child"));
|
|
|
|
|
+
|
|
|
|
|
+ String out = service.switchRole(2L, "parent");
|
|
|
|
|
+
|
|
|
|
|
+ assertEquals("parent", out);
|
|
|
|
|
+ verify(userMapper).updateById(argThat(u -> "parent".equals(u.getRole())));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+ // Service:关键边界 / 越权拒绝
|
|
|
|
|
+ // ---------------------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("用户不存在 → 返回 null,不做任何更新")
|
|
|
|
|
+ void service_用户不存在_返回null() {
|
|
|
|
|
+ when(userMapper.selectById(99L)).thenReturn(null);
|
|
|
|
|
+
|
|
|
|
|
+ String out = service.switchRole(99L, "teacher");
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(out);
|
|
|
|
|
+ verify(userMapper, never()).updateById(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("当前角色为 admin → 抛异常,不做任何更新")
|
|
|
|
|
+ void service_当前角色为admin_禁止切换() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "admin", "admin"));
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.switchRole(1L, "parent"));
|
|
|
|
|
+ assertEquals("管理员角色不允许切换", ex.getMessage());
|
|
|
|
|
+ verify(userMapper, never()).updateById(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("未持有目标角色(roles 无 teacher)→ 抛异常,不做任何更新")
|
|
|
|
|
+ void service_未拥有目标角色_抛异常() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "parent", "parent"));
|
|
|
|
|
+
|
|
|
|
|
+ RuntimeException ex = assertThrows(RuntimeException.class,
|
|
|
|
|
+ () -> service.switchRole(1L, "teacher"));
|
|
|
|
|
+ assertEquals("您没有该角色权限", ex.getMessage());
|
|
|
|
|
+ verify(userMapper, never()).updateById(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ @Test
|
|
|
|
|
+ @DisplayName("边界:非法目标角色(不在 parent/child/后台角色白名单)→ 返回 null,不做更新")
|
|
|
|
|
+ void service_非法目标角色_返回null() {
|
|
|
|
|
+ when(userMapper.selectById(1L)).thenReturn(user(1L, "parent", "hacker"));
|
|
|
|
|
+
|
|
|
|
|
+ String out = service.switchRole(1L, "hacker");
|
|
|
|
|
+
|
|
|
|
|
+ assertNull(out);
|
|
|
|
|
+ verify(userMapper, never()).updateById(any());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ========================================================================
|
|
|
|
|
+ // 工具
|
|
|
|
|
+ // ========================================================================
|
|
|
|
|
+
|
|
|
|
|
+ private Map<String, String> body(String role) {
|
|
|
|
|
+ Map<String, String> b = new HashMap<>();
|
|
|
|
|
+ b.put("role", role);
|
|
|
|
|
+ return b;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private User user(Long id, String role, String... ownedRoles) {
|
|
|
|
|
+ User u = new User();
|
|
|
|
|
+ u.setId(id);
|
|
|
|
|
+ u.setRole(role);
|
|
|
|
|
+ u.setRoles(ownedRoles == null ? null : Arrays.asList(ownedRoles));
|
|
|
|
|
+ return u;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 反射注入私有字段(贴合既有 unit 测试风格,如 GuidePlanReviewControllerAuthTest) */
|
|
|
|
|
+ private static void setField(Object target, String name, Object value) throws Exception {
|
|
|
|
|
+ Field f = target.getClass().getDeclaredField(name);
|
|
|
|
|
+ f.setAccessible(true);
|
|
|
|
|
+ f.set(target, value);
|
|
|
|
|
+ }
|
|
|
|
|
+}
|