跨校身份修订
ci-userbase-service / build-and-push (push) Successful in 44s

This commit is contained in:
chenxing
2026-08-27 20:14:12 +08:00
parent ebbaeee181
commit c588dedea7
31 changed files with 1811 additions and 328 deletions
@@ -0,0 +1,137 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.entity.SysUserRoleRel;
import com.yunhai.userbase.enums.StatusEnum;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserCurrentRoleRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRoleRelRepository;
import com.yunhai.userbase.service.ISysGroupService;
import com.yunhai.userbase.service.resolve.MainGroupResolver;
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.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* IdentityService 主身份动态解析(resolveMainIdentity / resolveMainIdentityByUser)单元测试。
* 规则:主组 → 主校 → 该校正常态身份行中 sortNo 最小、并列取 id 最小。
*/
class IdentityServiceTest {
@InjectMocks
private IdentityService identityService;
@Mock
private SysUserRoleRelRepository sysUserRoleRelRepository;
@Mock
private SysUserCurrentRoleRepository sysUserCurrentRoleRepository;
@Mock
private SysGroupRepository sysGroupRepository;
@Mock
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Mock
private MainGroupResolver mainGroupResolver;
@Mock
private ISysGroupService sysGroupService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
private SysUserGroupRel groupRel(Integer groupId, Integer isMain) {
SysUserGroupRel r = new SysUserGroupRel();
r.setUserId(1);
r.setGroupId(groupId);
r.setIsMain(isMain);
return r;
}
private SysUserRoleRel roleRel(Integer id, Integer userId, Integer schoolId, Integer sortNo) {
SysUserRoleRel r = new SysUserRoleRel();
r.setId(id);
r.setUserId(userId);
r.setSchoolId(schoolId);
r.setSortNo(sortNo);
r.setStatus(StatusEnum.NORMAL);
return r;
}
// ===================== 单用户 resolveMainIdentity =====================
/** 无组关系 → null */
@Test
void resolveMainIdentity_noGroup_returnsNull() {
when(mainGroupResolver.resolve(1)).thenReturn(Optional.empty());
assertNull(identityService.resolveMainIdentity(1));
}
/** 主校无身份行(身份行全在其它学校)→ null */
@Test
void resolveMainIdentity_mainSchoolHasNoRows_returnsNull() {
when(mainGroupResolver.resolve(1)).thenReturn(Optional.of(groupRel(16, 1)));
when(sysGroupService.resolveSchoolId(16)).thenReturn(16);
when(sysUserRoleRelRepository.findByUserIdAndStatus(1, StatusEnum.NORMAL))
.thenReturn(Collections.singletonList(roleRel(10, 1, 20, 0)));
assertNull(identityService.resolveMainIdentity(1));
}
/** 主校多行:sortNo 最小、并列取 id 最小 */
@Test
void resolveMainIdentity_picksMinSortNoThenMinId() {
when(mainGroupResolver.resolve(1)).thenReturn(Optional.of(groupRel(16, 1)));
when(sysGroupService.resolveSchoolId(16)).thenReturn(16);
List<SysUserRoleRel> rels = Arrays.asList(
roleRel(30, 1, 16, 2),
roleRel(31, 1, 16, 1),
roleRel(25, 1, 16, 1), // 与 31 同 sortNo → 取 id 小者
roleRel(10, 1, 20, 0)); // 其它学校,不参与
when(sysUserRoleRelRepository.findByUserIdAndStatus(1, StatusEnum.NORMAL)).thenReturn(rels);
SysUserRoleRel main = identityService.resolveMainIdentity(1);
assertEquals(Integer.valueOf(25), main.getId());
}
// ===================== 批量 resolveMainIdentityByUser =====================
/** 批量:3 次查询路径,无主组/主校无行的用户跳过 */
@Test
void resolveMainIdentityByUser_skipsUsersWithoutMain() {
Map<Integer, SysUserGroupRel> mainGroupMap = new HashMap<>();
mainGroupMap.put(1, groupRel(16, 1)); // 用户 1:主组 16
mainGroupMap.put(2, groupRel(20, 1)); // 用户 2:主组 20
when(mainGroupResolver.resolveByUserIds(Arrays.asList(1, 2, 3))).thenReturn(mainGroupMap);
Map<Integer, Integer> schoolByGroup = new HashMap<>();
schoolByGroup.put(16, 16);
schoolByGroup.put(20, 20);
when(sysGroupService.resolveSchoolIds(Arrays.asList(16, 20))).thenReturn(schoolByGroup);
when(sysUserRoleRelRepository.findByUserIdInAndStatus(Arrays.asList(1, 2, 3), StatusEnum.NORMAL))
.thenReturn(Arrays.asList(
roleRel(30, 1, 16, 1),
roleRel(31, 1, 16, 2),
roleRel(40, 2, 99, 0))); // 用户 2 主校 20 无行 → 跳过
Map<Integer, SysUserRoleRel> result = identityService.resolveMainIdentityByUser(Arrays.asList(1, 2, 3));
assertEquals(1, result.size());
assertEquals(Integer.valueOf(30), result.get(1).getId());
}
/** 批量:空入参 → 空映射 */
@Test
void resolveMainIdentityByUser_emptyInput_returnsEmptyMap() {
assertTrue(identityService.resolveMainIdentityByUser(Collections.emptyList()).isEmpty());
}
}
@@ -0,0 +1,157 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.platform.utils.exception.DException;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.enums.GroupTypeEnum;
import com.yunhai.userbase.repository.SysGroupRepository;
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.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.when;
/**
* SysGroupService.resolveSchoolId 单元测试(《用户身份-学校维度落地方案》§3 核心规则)。
* <br>规则:沿祖先链(含自身)——① 最近的学校组 → 该校组 id;② 无学校组则最近的区域组 → 该区域组 id;
* ③ 都没有 → 根节点 id。
*/
class SysGroupServiceIdentityTest {
@InjectMocks
private SysGroupService sysGroupService;
@Mock
private SysGroupRepository sysGroupRepository;
private SysGroup group(Integer id, Integer parentId, GroupTypeEnum type) {
SysGroup g = new SysGroup();
g.setId(id);
g.setParentId(parentId);
g.setType(type);
return g;
}
@BeforeEach
void setUp() {
MockitoAnnotations.initMocks(this);
// 根节点:parent_id=0,id=100(唯一真实根)
when(sysGroupRepository.findByParentIdOrderBySortAsc(0)).thenReturn(Collections.singletonList(group(100, 0, null)));
}
private void stubChain(SysGroup... chain) {
for (SysGroup g : chain) {
when(sysGroupRepository.findById(g.getId())).thenReturn(Optional.of(g));
}
}
/** 组自身即学校组 -> 返回自身 id */
@Test
void resolveSchoolId_selfIsSchool() {
stubChain(group(10, 100, GroupTypeEnum.SCHOOL_GROUP));
assertEquals(Integer.valueOf(10), sysGroupService.resolveSchoolId(10));
}
/** 链上最近学校组:普通组(7) -> 教师组(8) -> 学校组(10) -> 根 */
@Test
void resolveSchoolId_nearestSchoolInChain() {
stubChain(
group(7, 8, GroupTypeEnum.NORMAL_GROUP),
group(8, 10, GroupTypeEnum.TEACHER_GROUP),
group(10, 100, GroupTypeEnum.SCHOOL_GROUP));
assertEquals(Integer.valueOf(10), sysGroupService.resolveSchoolId(7));
}
/** 无学校组但链上有区域组:普通组(7) -> 学段组(6) -> 区域组(5) -> 根 -> 返回区域组 id */
@Test
void resolveSchoolId_noSchool_fallbackArea() {
stubChain(
group(7, 6, GroupTypeEnum.NORMAL_GROUP),
group(6, 5, GroupTypeEnum.STAGE_GROUP),
group(5, 100, GroupTypeEnum.AREA_GROUP));
assertEquals(Integer.valueOf(5), sysGroupService.resolveSchoolId(7));
}
/** 学校组优先于其上方的区域组:普通组 -> 学校组 -> 学段组 -> 区域组 -> 返回学校组 id */
@Test
void resolveSchoolId_schoolWinsOverAreaAbove() {
stubChain(
group(7, 10, GroupTypeEnum.NORMAL_GROUP),
group(10, 6, GroupTypeEnum.SCHOOL_GROUP),
group(6, 5, GroupTypeEnum.STAGE_GROUP),
group(5, 100, GroupTypeEnum.AREA_GROUP));
assertEquals(Integer.valueOf(10), sysGroupService.resolveSchoolId(7));
}
/** 区域组自身:无学校组、最近区域组即自身 -> 返回自身 */
@Test
void resolveSchoolId_selfIsArea() {
stubChain(group(5, 100, GroupTypeEnum.AREA_GROUP));
assertEquals(Integer.valueOf(5), sysGroupService.resolveSchoolId(5));
}
/** 直属根节点(无学校/区域)-> 根节点 id */
@Test
void resolveSchoolId_rootDirectFallbackToRoot() {
stubChain(group(7, 100, GroupTypeEnum.NORMAL_GROUP));
assertEquals(Integer.valueOf(100), sysGroupService.resolveSchoolId(7));
}
/** 组不存在(找不到实体)-> 兜底根节点 id(与「无学校/区域回根」一致) */
@Test
void resolveSchoolId_missingGroupFallbackToRoot() {
when(sysGroupRepository.findById(anyInt())).thenReturn(Optional.empty());
assertEquals(Integer.valueOf(100), sysGroupService.resolveSchoolId(999));
}
/** groupId 为空 -> 抛异常 */
@Test
void resolveSchoolId_nullGroupThrows() {
assertThrows(DException.class, () -> sysGroupService.resolveSchoolId(null));
}
// ===================== 批量 resolveSchoolIds =====================
/** 批量结果与单查一致:学校组优先 → 区域组 → 根 */
@Test
void resolveSchoolIds_batchMatchesSingle() {
List<SysGroup> all = Arrays.asList(
group(100, 0, null),
group(5, 100, GroupTypeEnum.AREA_GROUP),
group(10, 5, GroupTypeEnum.SCHOOL_GROUP),
group(8, 10, GroupTypeEnum.TEACHER_GROUP),
group(7, 8, GroupTypeEnum.NORMAL_GROUP),
group(20, 100, GroupTypeEnum.SCHOOL_GROUP));
when(sysGroupRepository.findAll()).thenReturn(all);
Map<Integer, Integer> result = sysGroupService.resolveSchoolIds(Arrays.asList(7, 5, 20));
assertEquals(3, result.size());
assertEquals(Integer.valueOf(10), result.get(7)); // 教师组 → 学校组 10
assertEquals(Integer.valueOf(5), result.get(5)); // 区域组自身
assertEquals(Integer.valueOf(20), result.get(20)); // 学校组自身
}
/** 组不存在(悬挂关系)→ 根节点 id(与 resolveSchoolId 单查一致) */
@Test
void resolveSchoolIds_missingGroupFallbackToRoot() {
when(sysGroupRepository.findAll()).thenReturn(Collections.singletonList(group(100, 0, null)));
Map<Integer, Integer> result = sysGroupService.resolveSchoolIds(Collections.singletonList(999));
assertEquals(Integer.valueOf(100), result.get(999));
}
/** 空入参 → 空映射 */
@Test
void resolveSchoolIds_emptyInputReturnsEmptyMap() {
assertTrue(sysGroupService.resolveSchoolIds(Collections.emptyList()).isEmpty());
}
}
@@ -0,0 +1,180 @@
package com.yunhai.userbase.service.impl;
import com.yunhai.userbase.entity.SysGroup;
import com.yunhai.userbase.entity.SysUser;
import com.yunhai.userbase.entity.SysUserCurrentRole;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.entity.SysUserRoleRel;
import com.yunhai.userbase.repository.SysGroupRepository;
import com.yunhai.userbase.repository.SysUserCurrentRoleRepository;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
import com.yunhai.userbase.repository.SysUserRepository;
import com.yunhai.userbase.repository.SysUserRoleRelRepository;
import com.yunhai.userbase.service.ISysGroupService;
import com.yunhai.userbase.service.resolve.MainGroupResolver;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.springframework.context.ApplicationEventPublisher;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
/**
* SysUserService 单元测试:主归属组三级降级决策(changeGroups)+
* 转学后身份对齐回归(P3 去字段化后 ①②③ 保留段仍生效)。
*/
class SysUserServiceTest {
@InjectMocks
private SysUserService sysUserService;
@Mock
private SysUserRepository sysUserRepository;
@Mock
private SysGroupRepository sysGroupRepository;
@Mock
private SysUserGroupRelRepository sysUserGroupRelRepository;
@Mock
private SysUserRoleRelRepository sysUserRoleRelRepository;
@Mock
private SysUserCurrentRoleRepository sysUserCurrentRoleRepository;
@Mock
private ISysGroupService sysGroupService;
@Mock
private MainGroupResolver mainGroupResolver;
@Mock
private ApplicationEventPublisher eventPublisher;
@Mock
private IdentityService identityService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
private SysUserGroupRel rel(Integer groupId, Integer isMain) {
SysUserGroupRel r = new SysUserGroupRel();
r.setGroupId(groupId);
r.setIsMain(isMain);
return r;
}
private SysUserRoleRel roleRel(Integer id, Integer userId, Integer schoolId) {
SysUserRoleRel r = new SysUserRoleRel();
r.setId(id);
r.setUserId(userId);
r.setSchoolId(schoolId);
return r;
}
// ===================== 主归属组三级降级决策 =====================
/** ① 显式 mainGroupId 优先(不受旧主组影响) */
@Test
void decide_whenExplicitMainGroupId_shouldUseIt() {
Integer main = SysUserService.decideEffectiveMainGroupId(
30, Arrays.asList(16, 30), rel(16, 1));
assertEquals(Integer.valueOf(30), main);
}
/** ② 未传时沿用旧主组(旧主组 ∈ 新集合)——「主组没被勾掉就不动」 */
@Test
void decide_whenNoExplicitAndOldMainInNewGroups_shouldKeepOldMain() {
Integer main = SysUserService.decideEffectiveMainGroupId(
null, Arrays.asList(29, 16), rel(16, 1));
assertEquals(Integer.valueOf(16), main);
}
/** ② 未传、无旧主组(脏数据)→ ③ groupIds 首项 */
@Test
void decide_whenNoExplicitAndNoOldMain_shouldFallbackToFirstGroup() {
Integer main = SysUserService.decideEffectiveMainGroupId(
null, Arrays.asList(29, 16), null);
assertEquals(Integer.valueOf(29), main);
}
/** ③ 旧主组被勾掉(不在新集合)→ groupIds 首项(现状行为兜底) */
@Test
void decide_whenOldMainRemovedFromNewGroups_shouldFallbackToFirstGroup() {
Integer main = SysUserService.decideEffectiveMainGroupId(
null, Arrays.asList(29, 30), rel(16, 1));
assertEquals(Integer.valueOf(29), main);
}
// ===================== 转学后身份对齐回归(P3 去字段化后 ①②③ 保留段) =====================
/**
* changeGroups 转学场景(新组 30 替代旧组 16/20):删除 ④ is_main 置位段后,
* ① 学校集合推导、② 孤儿身份行清理(旧校 16 身份行删)、③ 激活身份滞留清理
* (current 指向旧校 16 删)必须仍生效;主校 30 身份行保留。
*/
@Test
@SuppressWarnings("unchecked")
void changeGroups_afterTransfer_shouldKeepOrphanAndCurrentStaleCleanup() {
SysUser user = new SysUser();
user.setId(1);
SysGroup group30 = new SysGroup();
group30.setId(30);
SysUserGroupRel oldMain = rel(16, 1);
oldMain.setId(1);
SysUserGroupRel oldExtra = rel(20, 0);
oldExtra.setId(2);
List<SysUserGroupRel> oldRels = Arrays.asList(oldMain, oldExtra);
SysUserGroupRel newRel = new SysUserGroupRel();
newRel.setUserId(1);
newRel.setGroupId(30);
List<SysUserGroupRel> newRels = Collections.singletonList(newRel);
when(sysUserRepository.findAllById(Collections.singletonList(1)))
.thenReturn(Collections.singletonList(user));
when(sysGroupRepository.findAllById(Collections.singletonList(30)))
.thenReturn(Collections.singletonList(group30));
// 改组前读旧关系(status 继承)→ 对齐时读新关系(schoolSet 推导)
when(sysUserGroupRelRepository.findByUserId(1)).thenReturn(oldRels, newRels);
when(mainGroupResolver.resolve(oldRels)).thenReturn(oldMain);
when(sysGroupService.resolveSchoolId(30)).thenReturn(30);
SysUserRoleRel orphan = roleRel(10, 1, 16);
SysUserRoleRel kept = roleRel(11, 1, 30);
when(sysUserRoleRelRepository.findByUserId(1)).thenReturn(Arrays.asList(orphan, kept));
SysUserCurrentRole current = new SysUserCurrentRole();
current.setUserId(1);
current.setSchoolId(16);
when(sysUserCurrentRoleRepository.findByUserId(1)).thenReturn(Optional.of(current));
sysUserService.changeGroups(Collections.singletonList(1), Collections.singletonList(30), 30);
// ① 新组关系落库:目标组 30 置主(显式 mainGroupId)
ArgumentCaptor<List<SysUserGroupRel>> groupRelCaptor = ArgumentCaptor.forClass(List.class);
verify(sysUserGroupRelRepository).saveAll(groupRelCaptor.capture());
List<SysUserGroupRel> savedRels = groupRelCaptor.getValue();
assertEquals(1, savedRels.size());
assertEquals(Integer.valueOf(1), savedRels.get(0).getIsMain());
// ② 孤儿身份行(旧校 16)删除、主校 30 行保留(identity 断言,规避实体 equals 恒真问题)
ArgumentCaptor<SysUserRoleRel> deletedCaptor = ArgumentCaptor.forClass(SysUserRoleRel.class);
verify(sysUserRoleRelRepository).delete(deletedCaptor.capture());
assertSame(orphan, deletedCaptor.getValue());
ArgumentCaptor<List<SysUserRoleRel>> roleRelCaptor = ArgumentCaptor.forClass(List.class);
verify(sysUserRoleRelRepository).saveAll(roleRelCaptor.capture());
List<SysUserRoleRel> remaining = roleRelCaptor.getValue();
assertEquals(1, remaining.size());
assertSame(kept, remaining.get(0));
// ③ 激活身份滞留清理:current 指向旧校 16(不在新 schoolSet {30})→ 删除
verify(sysUserCurrentRoleRepository).delete(current);
}
}
@@ -0,0 +1,135 @@
package com.yunhai.userbase.service.resolve;
import com.yunhai.userbase.entity.SysUserGroupRel;
import com.yunhai.userbase.repository.SysUserGroupRelRepository;
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.List;
import java.util.Map;
import java.util.Optional;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
/**
* MainGroupResolver 单元测试:验证统一兜底契约
* 「isMain=1 取 id 最小;无 isMain=1 取 id 最小;空列表 → null」。
*/
class MainGroupResolverTest {
@InjectMocks
private MainGroupResolver mainGroupResolver;
@Mock
private SysUserGroupRelRepository sysUserGroupRelRepository;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
/** 构造关联行(id、userId、groupId、isMain) */
private SysUserGroupRel rel(Integer id, Integer userId, Integer groupId, Integer isMain) {
SysUserGroupRel r = new SysUserGroupRel();
r.setId(id);
r.setUserId(userId);
r.setGroupId(groupId);
r.setIsMain(isMain);
return r;
}
// ===================== 纯函数 resolve(List) =====================
/** 空列表 → null(resolve(null) 显式转 List 消除重载歧义) */
@Test
void resolve_whenEmptyList_shouldReturnNull() {
assertNull(mainGroupResolver.resolve(Collections.emptyList()));
assertNull(mainGroupResolver.resolve((List<SysUserGroupRel>) null));
}
@Test
void resolve_whenNoMain_shouldPickMinId() {
// 全部非主:取 id 最小(第一条顺序依赖被消除)
List<SysUserGroupRel> rels = Arrays.asList(
rel(7, 1, 30, 0),
rel(3, 1, 16, 0),
rel(9, 1, 29, 0));
SysUserGroupRel main = mainGroupResolver.resolve(rels);
assertEquals(Integer.valueOf(3), main.getId());
}
@Test
void resolve_whenMultipleMain_shouldPickMinIdAmongMains() {
// 脏数据多主:isMain=1 中取 id 最小(id=3 优先于 id=9)
List<SysUserGroupRel> rels = Arrays.asList(
rel(5, 1, 30, 0),
rel(9, 1, 29, 1),
rel(3, 1, 16, 1));
SysUserGroupRel main = mainGroupResolver.resolve(rels);
assertEquals(Integer.valueOf(3), main.getId());
assertEquals(Integer.valueOf(16), main.getGroupId());
}
@Test
void resolve_whenNullGroupId_shouldSkipDirtyRow() {
// groupId 为 null 的关系行(脏数据)不参与选择
List<SysUserGroupRel> rels = Arrays.asList(
rel(1, 1, null, 1),
rel(10, 1, 29, 0));
SysUserGroupRel main = mainGroupResolver.resolve(rels);
assertEquals(Integer.valueOf(10), main.getId());
// 全部为 null groupId → null
assertNull(mainGroupResolver.resolve(Collections.singletonList(rel(1, 1, null, 1))));
}
// ===================== 单用户 resolve(Integer) =====================
@Test
void resolve_byUserId_whenNoRels_shouldReturnEmpty() {
when(sysUserGroupRelRepository.findByUserId(1)).thenReturn(Collections.emptyList());
assertFalse(mainGroupResolver.resolve(1).isPresent());
}
@Test
void resolve_byUserId_shouldDelegateToPureFunction() {
when(sysUserGroupRelRepository.findByUserId(1)).thenReturn(Arrays.asList(
rel(7, 1, 30, 0),
rel(2, 1, 16, 1)));
Optional<SysUserGroupRel> main = mainGroupResolver.resolve(1);
assertTrue(main.isPresent());
assertEquals(Integer.valueOf(2), main.get().getId());
}
// ===================== 批量 resolveByUserIds =====================
@Test
void resolveByUserIds_whenEmptyInput_shouldReturnEmptyMap() {
assertTrue(mainGroupResolver.resolveByUserIds(Collections.emptyList()).isEmpty());
}
@Test
void resolveByUserIds_shouldResolvePerUserAndSkipUsersWithoutRels() {
when(sysUserGroupRelRepository.findByUserIdIn(Arrays.asList(1, 2, 3))).thenReturn(Arrays.asList(
rel(5, 1, 30, 0),
rel(1, 1, 16, 1),
rel(2, 2, 29, 0)));
Map<Integer, SysUserGroupRel> result = mainGroupResolver.resolveByUserIds(Arrays.asList(1, 2, 3));
assertEquals(2, result.size());
// 用户 1:多主/无主混合,取 isMain=1 中 id 最小
assertEquals(Integer.valueOf(1), result.get(1).getId());
// 用户 2:无主兜底 id 最小
assertEquals(Integer.valueOf(2), result.get(2).getId());
// 用户 3:无组关系,不出现
assertFalse(result.containsKey(3));
}
}