package com.yunhai.userbase.service.impl; import com.yunhai.userbase.entity.AppCategory; import com.yunhai.userbase.entity.AppModule; import com.yunhai.userbase.entity.ApplicationInfo; import com.yunhai.userbase.entity.SysGroup; import com.yunhai.userbase.entity.SysGroupAppRel; import com.yunhai.userbase.entity.SysGroupModulePerm; import com.yunhai.userbase.entity.SysUser; import com.yunhai.userbase.entity.SysUserGroupRel; import com.yunhai.userbase.enums.GroupTypeEnum; import com.yunhai.userbase.enums.UserTypeEnum; import com.yunhai.userbase.model.groupauthz.UserPermissionCategoryVo; import com.yunhai.userbase.model.groupauthz.UserPermissionModuleVo; import com.yunhai.userbase.repository.AppCategoryRepository; import com.yunhai.userbase.repository.AppModuleRepository; import com.yunhai.userbase.repository.ApplicationInfoRepository; import com.yunhai.userbase.repository.SysAppRoleModuleRelRepository; import com.yunhai.userbase.repository.SysAppRoleRepository; import com.yunhai.userbase.repository.SysGroupAppRelRepository; import com.yunhai.userbase.repository.SysGroupModulePermRepository; import com.yunhai.userbase.repository.SysGroupRepository; import com.yunhai.userbase.repository.SysUserAppRoleRelRepository; import com.yunhai.userbase.repository.SysUserGroupRelRepository; import com.yunhai.userbase.repository.SysUserRepository; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockitoAnnotations; import java.util.Arrays; import java.util.Collections; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.Mockito.when; /** * GroupAuthzService 权限计算单元测试。 * 重点验证「就近原则 + 子组继承闸门」:沿组的祖先链向上,取最近的有显式模块配置的组作为该应用唯一权限来源, * 只返回该来源组的模块集合(不向上合并更远的祖先);且来源组若为祖先,必须勾选子组继承(inheritFlag=true)才下放 * (修复「子组只勾1个却继承学校全部」「返回3个而非最近祖先的2个」「父级未勾子组继承仍被继承」三类 bug)。 */ class GroupAuthzServiceTest { @InjectMocks private GroupAuthzService groupAuthzService; @Mock private SysGroupAppRelRepository sysGroupAppRelRepository; @Mock private SysGroupModulePermRepository sysGroupModulePermRepository; @Mock private SysGroupRepository sysGroupRepository; @Mock private ApplicationInfoRepository applicationInfoRepository; @Mock private AppCategoryRepository appCategoryRepository; @Mock private AppModuleRepository appModuleRepository; @Mock private SysUserGroupRelRepository sysUserGroupRelRepository; @Mock private SysUserRepository sysUserRepository; @Mock private SysAppRoleRepository sysAppRoleRepository; @Mock private SysAppRoleModuleRelRepository sysAppRoleModuleRelRepository; @Mock private SysUserAppRoleRelRepository sysUserAppRoleRelRepository; @BeforeEach void setUp() { MockitoAnnotations.openMocks(this); initGroups(); } /** * 构造一组固定层级: * 5 教育云中心(根) * └─ 27 信阳教育局 * └─ 16 云海中学(学校) * ├─ 29 教师组 * │ └─ 31 英语组 * └─ 30 学生组 */ private void initGroups() { when(sysGroupRepository.findAll()).thenReturn(Arrays.asList( group(5, "教育云中心", 0, GroupTypeEnum.NORMAL_GROUP), group(27, "信阳教育局", 5, GroupTypeEnum.AREA_GROUP), group(16, "云海中学", 27, GroupTypeEnum.SCHOOL_GROUP), group(28, "信阳高级中学", 27, GroupTypeEnum.SCHOOL_GROUP), group(29, "教师组", 16, GroupTypeEnum.TEACHER_GROUP), group(30, "学生组", 16, GroupTypeEnum.STUDENT_GROUP), group(31, "英语组", 29, GroupTypeEnum.NORMAL_GROUP) )); } @Test void findUserPermissionTree_whenSelfGroupConfiguredApp_shouldNotInheritAncestorModules() { // user 1 仅属于英语组 31 when(sysUserRepository.findById(1)).thenReturn(Optional.of(user(1, UserTypeEnum.NORMAL))); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); // 祖先链上的应用开通:只有学校 16 开通了 100003 when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> { List ids = inv.getArgument(0); if (ids.contains(16)) { return Collections.singletonList(appRel(16, 100003)); } return Collections.emptyList(); }); ApplicationInfo app = new ApplicationInfo(); app.setId(100003); app.setName("通知公告"); app.setAppKey(100002); app.setCategoryId(2); when(applicationInfoRepository.findAllById(any())).thenReturn(Collections.singletonList(app)); AppCategory category = new AppCategory(); category.setId(2); category.setName("家校互动"); category.setSortNo(2); when(appCategoryRepository.findAllByOrderBySortNoAsc()).thenReturn(Collections.singletonList(category)); AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(Arrays.asList(m8, m9, m10)); // 权限记录:英语组 31 只有模块 8;学校 16 有 8/9/10 且都允许继承 when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList())) .thenReturn(Arrays.asList( perm(31, 8, true), perm(16, 8, true), perm(16, 9, true), perm(16, 10, true))); List result = groupAuthzService.findUserPermissionTree(31, 1); assertEquals(1, result.size(), "应只有一个分类"); assertEquals("家校互动", result.get(0).getName()); assertEquals(1, result.get(0).getApps().size(), "应只有一个应用"); assertEquals(1, result.get(0).getApps().get(0).getModules().size(), "只应返回模块 8"); assertEquals(8, result.get(0).getApps().get(0).getModules().get(0).getId()); } @Test void findUserPermissionTree_whenSelfGroupNotConfiguredApp_shouldInheritFromSchool() { // user 2 仅属于学生组 30,学生组没有配置通知公告应用的任何模块 when(sysUserRepository.findById(2)).thenReturn(Optional.of(user(2, UserTypeEnum.NORMAL))); when(sysUserGroupRelRepository.findByUserId(2)).thenReturn( Collections.singletonList(rel(2, 30, 1))); when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> { List ids = inv.getArgument(0); if (ids.contains(16)) { return Collections.singletonList(appRel(16, 100003)); } return Collections.emptyList(); }); ApplicationInfo app = new ApplicationInfo(); app.setId(100003); app.setName("通知公告"); app.setAppKey(100002); app.setCategoryId(2); when(applicationInfoRepository.findAllById(any())).thenReturn(Collections.singletonList(app)); AppCategory category = new AppCategory(); category.setId(2); category.setName("家校互动"); category.setSortNo(2); when(appCategoryRepository.findAllByOrderBySortNoAsc()).thenReturn(Collections.singletonList(category)); AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(Arrays.asList(m8, m9, m10)); // 学校 16 三个模块都开放继承;学生组 30 没有任何记录 when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList())) .thenReturn(Arrays.asList( perm(16, 8, true), perm(16, 9, true), perm(16, 10, true))); List result = groupAuthzService.findUserPermissionTree(30, 2); assertEquals(3, result.get(0).getApps().get(0).getModules().size(), "未配置应用时,应继承学校的全部 3 个模块"); } @Test void hasModulePermission_whenSelfGroupConfiguredApp_onlySelfRecordsCount() { AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findById(9)).thenReturn(Optional.of(m9)); when(sysUserAppRoleRelRepository.findByUserId(1)).thenReturn(Collections.emptyList()); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); when(sysGroupAppRelRepository.existsByGroupIdAndAppId(31, 100003)).thenReturn(true); when(appModuleRepository.findByAppIdIn(Collections.singletonList(100003))).thenReturn(Arrays.asList(m8, m9, m10)); // 英语组 31 配置了应用 100003(模块 8);学校 16 有模块 9(但 31 已就近接管,不应继承) when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn( Arrays.asList(8, 9, 10), Arrays.asList(31, 29, 16, 27, 5))) .thenReturn(Arrays.asList(perm(31, 8, true), perm(16, 9, true))); assertFalse(groupAuthzService.hasModulePermission(1, 9), "英语组已配置通知公告应用(就近接管),不应再继承学校的已发通知模块"); } @Test void hasModulePermission_whenSelfGroupHasTheModule_shouldReturnTrue() { AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findById(8)).thenReturn(Optional.of(m8)); when(sysUserAppRoleRelRepository.findByUserId(1)).thenReturn(Collections.emptyList()); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); when(sysGroupAppRelRepository.existsByGroupIdAndAppId(31, 100003)).thenReturn(true); when(appModuleRepository.findByAppIdIn(Collections.singletonList(100003))).thenReturn(Arrays.asList(m8, m9, m10)); when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn( Arrays.asList(8, 9, 10), Arrays.asList(31, 29, 16, 27, 5))) .thenReturn(Arrays.asList(perm(31, 8, true), perm(16, 8, true))); assertTrue(groupAuthzService.hasModulePermission(1, 8), "英语组自身已配置模块 8,应返回 true"); } @Test void findUserPermissionTree_whenSelfGroupEmpty_shouldUseNearestAncestorTeacherGroup() { // user 1 仅属于英语组 31;英语组(31)无任何模块授权,教师组(29)有模块9/10,学校(16)有8/9/10 when(sysUserRepository.findById(1)).thenReturn(Optional.of(user(1, UserTypeEnum.NORMAL))); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> { List ids = inv.getArgument(0); if (ids.contains(16)) { return Collections.singletonList(appRel(16, 100003)); } return Collections.emptyList(); }); ApplicationInfo app = new ApplicationInfo(); app.setId(100003); app.setName("通知公告"); app.setAppKey(100002); app.setCategoryId(2); when(applicationInfoRepository.findAllById(any())).thenReturn(Collections.singletonList(app)); AppCategory category = new AppCategory(); category.setId(2); category.setName("家校互动"); category.setSortNo(2); when(appCategoryRepository.findAllByOrderBySortNoAsc()).thenReturn(Collections.singletonList(category)); AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(Arrays.asList(m8, m9, m10)); // 英语组31空;教师组29有9/10;学校16有8/9/10 when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList())) .thenReturn(Arrays.asList( perm(29, 9, true), perm(29, 10, true), perm(16, 8, true), perm(16, 9, true), perm(16, 10, true))); List result = groupAuthzService.findUserPermissionTree(31, 1); assertEquals(1, result.size(), "应只有一个分类"); assertEquals(1, result.get(0).getApps().size(), "应只有一个应用"); assertEquals(2, result.get(0).getApps().get(0).getModules().size(), "本组空→取最近教师组29的2个模块,不应合并学校组多出的模块8"); Set moduleIds = new LinkedHashSet<>(); for (UserPermissionModuleVo mv : result.get(0).getApps().get(0).getModules()) { moduleIds.add(mv.getId()); } assertTrue(moduleIds.contains(9) && moduleIds.contains(10) && moduleIds.size() == 2, "应仅含模块 9 和 10,不含学校组多出的模块 8"); } @Test void findUserPermissionTree_whenNearestAncestorAllNotInheritable_returnsEmpty() { // 英语组31空;教师组29有9/10但 inheritFlag=false(未勾子组继承);学校16有8/9/10 when(sysUserRepository.findById(1)).thenReturn(Optional.of(user(1, UserTypeEnum.NORMAL))); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> { List ids = inv.getArgument(0); if (ids.contains(16)) { return Collections.singletonList(appRel(16, 100003)); } return Collections.emptyList(); }); ApplicationInfo app = new ApplicationInfo(); app.setId(100003); app.setName("通知公告"); app.setAppKey(100002); app.setCategoryId(2); when(applicationInfoRepository.findAllById(any())).thenReturn(Collections.singletonList(app)); AppCategory category = new AppCategory(); category.setId(2); category.setName("家校互动"); category.setSortNo(2); when(appCategoryRepository.findAllByOrderBySortNoAsc()).thenReturn(Collections.singletonList(category)); AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(Arrays.asList(m8, m9, m10)); // 教师组29(最近来源)所有模块 inheritFlag=false;即便学校16开放,也不应被继承 when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList())) .thenReturn(Arrays.asList( perm(29, 9, false), perm(29, 10, false), perm(16, 8, true), perm(16, 9, true), perm(16, 10, true))); List result = groupAuthzService.findUserPermissionTree(31, 1); // 最近来源(教师组)对该应用所有模块都未勾子组继承 → 本组该应用为空,不应合并学校组 assertTrue(result.isEmpty(), "最近祖先(教师组)未勾子组继承,应返回空,不应从学校组继承任何模块"); } @Test void findUserPermissionTree_whenNearestAncestorPartiallyInheritable_returnsOnlyInheritable() { // 英语组31空;教师组29有9(inheritFlag=true)/10(inheritFlag=false);学校16有8/9/10 when(sysUserRepository.findById(1)).thenReturn(Optional.of(user(1, UserTypeEnum.NORMAL))); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); when(sysGroupAppRelRepository.findByGroupIdIn(anyList())).thenAnswer(inv -> { List ids = inv.getArgument(0); if (ids.contains(16)) { return Collections.singletonList(appRel(16, 100003)); } return Collections.emptyList(); }); ApplicationInfo app = new ApplicationInfo(); app.setId(100003); app.setName("通知公告"); app.setAppKey(100002); app.setCategoryId(2); when(applicationInfoRepository.findAllById(any())).thenReturn(Collections.singletonList(app)); AppCategory category = new AppCategory(); category.setId(2); category.setName("家校互动"); category.setSortNo(2); when(appCategoryRepository.findAllByOrderBySortNoAsc()).thenReturn(Collections.singletonList(category)); AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findByAppIdIn(anyList())).thenReturn(Arrays.asList(m8, m9, m10)); when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn(anyList(), anyList())) .thenReturn(Arrays.asList( perm(29, 9, true), perm(29, 10, false), perm(16, 8, true), perm(16, 9, true), perm(16, 10, true))); List result = groupAuthzService.findUserPermissionTree(31, 1); assertEquals(1, result.size(), "应只有一个分类"); assertEquals(1, result.get(0).getApps().size(), "应只有一个应用"); assertEquals(1, result.get(0).getApps().get(0).getModules().size(), "最近来源(教师组)只有模块9勾了子组继承,应只返回模块9,模块10与学校组模块8均不继承"); assertEquals(9, result.get(0).getApps().get(0).getModules().get(0).getId()); } @Test void hasModulePermission_whenSelfGroupEmpty_nearestAncestorIsTeacherGroup() { // 英语组31空,教师组29有9/10,学校16有8/9/10 AppModule m8 = module(8, 100003, "已收通知"); AppModule m9 = module(9, 100003, "已发通知"); AppModule m10 = module(10, 100003, "全部通知"); when(appModuleRepository.findById(8)).thenReturn(Optional.of(m8)); when(appModuleRepository.findById(9)).thenReturn(Optional.of(m9)); when(appModuleRepository.findById(10)).thenReturn(Optional.of(m10)); when(sysUserAppRoleRelRepository.findByUserId(1)).thenReturn(Collections.emptyList()); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn( Collections.singletonList(rel(1, 31, 1))); when(sysGroupAppRelRepository.existsByGroupIdAndAppId(31, 100003)).thenReturn(true); when(appModuleRepository.findByAppIdIn(Collections.singletonList(100003))).thenReturn(Arrays.asList(m8, m9, m10)); when(sysGroupModulePermRepository.findByModuleIdInAndGroupIdIn( Arrays.asList(8, 9, 10), Arrays.asList(31, 29, 16, 27, 5))) .thenReturn(Arrays.asList( perm(29, 9, true), perm(29, 10, true), perm(16, 8, true), perm(16, 9, true), perm(16, 10, true))); assertFalse(groupAuthzService.hasModulePermission(1, 8), "英语组为空→最近来源是教师组29,学校组多勾的模块8不应被继承"); assertTrue(groupAuthzService.hasModulePermission(1, 9), "教师组29勾选的模块9应可访问"); assertTrue(groupAuthzService.hasModulePermission(1, 10), "教师组29勾选的模块10应可访问"); } @Test void hasModulePermission_whenSuperAdmin_shouldReturnTrueRegardlessOfGroup() { // 超级管理员:type=SUPER_ADMIN,且不归属任何组(超管通常不在任何组下) when(sysUserRepository.findById(1)).thenReturn(Optional.of(user(1, UserTypeEnum.SUPER_ADMIN))); when(sysUserGroupRelRepository.findByUserId(1)).thenReturn(Collections.emptyList()); assertTrue(groupAuthzService.hasModulePermission(1, 8), "超级管理员应直接拥有全部模块权限,无需归属组/角色(修复「超管无组→返回false」的 bug)"); assertTrue(groupAuthzService.hasModulePermission(1, 100), "超级管理员对任意模块都应返回 true"); } // ===================== helper ===================== private SysGroup group(Integer id, String name, Integer parentId, GroupTypeEnum type) { SysGroup g = new SysGroup(); g.setId(id); g.setName(name); g.setParentId(parentId); g.setType(type); return g; } private SysUser user(Integer id, UserTypeEnum type) { SysUser u = new SysUser(); u.setId(id); u.setType(type); u.setAccount("u" + id); u.setRealName("用户" + id); return u; } private SysUserGroupRel rel(Integer userId, Integer groupId, Integer isMain) { SysUserGroupRel r = new SysUserGroupRel(); r.setUserId(userId); r.setGroupId(groupId); r.setIsMain(isMain); return r; } private SysGroupAppRel appRel(Integer groupId, Integer appId) { SysGroupAppRel r = new SysGroupAppRel(); r.setGroupId(groupId); r.setAppId(appId); return r; } private SysGroupModulePerm perm(Integer groupId, Integer moduleId, boolean inherit) { SysGroupModulePerm p = new SysGroupModulePerm(); p.setGroupId(groupId); p.setModuleId(moduleId); p.setInheritFlag(inherit); return p; } private ApplicationInfo app(Integer id, String name, Integer categoryId) { ApplicationInfo a = new ApplicationInfo(); a.setId(id); a.setName(name); a.setCategoryId(categoryId); return a; } private AppModule module(Integer id, Integer appId, String name) { AppModule m = new AppModule(); m.setId(id); m.setAppId(appId); m.setName(name); m.setCustomId(String.valueOf(id)); m.setLevel(1); return m; } }