Files
UserBaseService/src/test/java/com/yunhai/userbase/service/impl/GroupAuthzServiceTest.java
T
chenxing f55c865ab1
ci-userbase-service / build-and-push (push) Successful in 41s
模块权限修订,超管权限
2026-08-11 22:43:09 +08:00

505 lines
23 KiB
Java
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 权限计算单元测试。
* 重点验证「就近原则 + 子组继承闸门」:沿组的祖先链向上,取<b>最近</b>的有显式模块配置的组作为该应用唯一权限来源,
* 只返回该来源组的模块集合(不向上合并更远的祖先);且<b>来源组若为祖先,必须勾选子组继承(inheritFlag=true)才下放</b>
* (修复「子组只勾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<Integer> 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<UserPermissionCategoryVo> 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<Integer> 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<UserPermissionCategoryVo> 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<Integer> 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<UserPermissionCategoryVo> 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<Integer> 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<Integer> 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<UserPermissionCategoryVo> 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<Integer> 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<UserPermissionCategoryVo> 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;
}
}