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;
}
}