package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.ListUtil; import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; import cn.iocoder.yudao.framework.common.exception.ServiceException; import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.im.service.websocket.notification.message.ImGroupMessageNotification; import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.springframework.dao.DuplicateKeyException; import java.util.List; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; /** * {@link ImGroupMemberServiceImpl} 的单元测试 * * @author 芋道源码 */ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private ImGroupMemberServiceImpl groupMemberService; @Mock private ImGroupMemberMapper groupMemberMapper; @Mock private ImWebSocketService webSocketService; @Mock private cn.iocoder.yudao.module.im.service.message.ImGroupMessageService groupMessageService; // ========== addGroupMember ========== @Test public void testAddGroupMember_newInsert() { // 准备:成员记录不存在 when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null); // 调用 ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); // 断言:执行了 insert,返回记录的 status 为 ENABLE assertNotNull(result); assertEquals(10L, result.getGroupId()); assertEquals(1L, result.getUserId()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); assertNotNull(result.getJoinTime()); verify(groupMemberMapper).insert(any(ImGroupMemberDO.class)); } @Test public void testAddGroupMember_existingEnabledReturns() { // 准备:已存在且 ENABLE,只返回已有记录,不做其它操作 ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); assertEquals(50L, result.getId()); verify(groupMemberMapper, never()).insert(any(ImGroupMemberDO.class)); verify(groupMemberMapper, never()).updateById(any(ImGroupMemberDO.class)); } @Test public void testAddGroupMember_existingDisabledRecovers() { // 准备:已存在且 DISABLE,应重置为 ENABLE 并重置 role 为 MEMBER ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.DISABLE.getStatus()) .role(ImGroupMemberRoleEnum.ADMIN.getRole()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); verify(groupMemberMapper).updateRejoinFields(eq(50L), eq(CommonStatusEnum.ENABLE.getStatus()), any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); assertEquals(ImGroupMemberRoleEnum.NORMAL.getRole(), result.getRole()); assertNull(result.getQuitTime()); assertNull(result.getMuteEndTime()); } @Test public void testAddGroupMember_duplicateKeyFallsBackToSelect() { // 准备:第一次 select 返回 null,insert 抛 DuplicateKey,再次 select 返回已插入记录 ImGroupMemberDO inserted = ImGroupMemberDO.builder().id(80L).groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null).thenReturn(inserted); when(groupMemberMapper.insert(any(ImGroupMemberDO.class))) .thenThrow(new DuplicateKeyException("concurrent insert")); // 调用:冲突后降级 select ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); assertNotNull(result); assertEquals(80L, result.getId()); } // ========== addGroupMembers ========== @Test public void testAddGroupMembers_mixedInsertAndUpdate() { // 准备:用户 2 不存在(新增),用户 3 已存在且 DISABLE(恢复),用户 4 已存在且 ENABLE(跳过) ImGroupMemberDO exist3 = ImGroupMemberDO.builder().id(30L).groupId(10L).userId(3L) .status(CommonStatusEnum.DISABLE.getStatus()).build(); ImGroupMemberDO exist4 = ImGroupMemberDO.builder().id(40L).groupId(10L).userId(4L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) .thenReturn(ListUtil.of(exist3, exist4)); // 调用 groupMemberService.addGroupMembers(10L, ListUtil.of(2L, 3L, 4L)); // 断言:updates 只有用户 3;inserts 只有用户 2 verify(groupMemberMapper).updateRejoinFields(eq(30L), eq(CommonStatusEnum.ENABLE.getStatus()), any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); verify(groupMemberMapper).insertBatch(argThat((List list) -> list.size() == 1 && list.get(0).getUserId().equals(2L))); } @Test public void testAddGroupMembers_allExisting_onlyUpdates() { // 准备:传入的 3 个用户都已有记录(全部 DISABLE) → 只做 update,不做 insert List existing = ListUtil.of( ImGroupMemberDO.builder().id(1L).groupId(10L).userId(2L) .status(CommonStatusEnum.DISABLE.getStatus()).build(), ImGroupMemberDO.builder().id(2L).groupId(10L).userId(3L) .status(CommonStatusEnum.DISABLE.getStatus()).build() ); when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) .thenReturn(existing); groupMemberService.addGroupMembers(10L, ListUtil.of(2L, 3L)); verify(groupMemberMapper, times(2)).updateRejoinFields(anyLong(), eq(CommonStatusEnum.ENABLE.getStatus()), any(), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), isNull(), isNull()); verify(groupMemberMapper, never()).insertBatch(anyList()); } @Test public void testAddGroupMembers_allNew_onlyInserts() { // 准备:都不存在 → 只做 insert when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) .thenReturn(ListUtil.of()); groupMemberService.addGroupMembers(10L, ListUtil.of(2L, 3L)); verify(groupMemberMapper, never()).updateRejoinFields(anyLong(), anyInt(), any(), anyInt(), any(), any()); verify(groupMemberMapper).insertBatch(anyList()); } @Test public void testAddGroupMembers_allExistingEnabled_nothingHappens() { // 准备:都已存在且 ENABLE → 既不 update 也不 insert List existing = ListUtil.of( ImGroupMemberDO.builder().id(1L).groupId(10L).userId(2L) .status(CommonStatusEnum.ENABLE.getStatus()).build() ); when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) .thenReturn(existing); groupMemberService.addGroupMembers(10L, ListUtil.of(2L)); verify(groupMemberMapper, never()).updateRejoinFields(anyLong(), anyInt(), any(), anyInt(), any(), any()); verify(groupMemberMapper, never()).insertBatch(anyList()); } @Test public void testAddGroupMembers_batchInsertDuplicateFallback() { // 准备:两个新增成员,批量插入失败时降级为逐个 addGroupMember when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) .thenReturn(ListUtil.of()); doThrow(new DuplicateKeyException("concurrent batch insert")) .when(groupMemberMapper).insertBatch(anyList()); // addGroupMember 单条兜底逻辑 when(groupMemberMapper.selectByGroupIdAndUserId(eq(10L), anyLong())).thenReturn(null); // 调用 groupMemberService.addGroupMembers(10L, ListUtil.of(2L, 3L)); // 断言:降级为逐条调用 insert verify(groupMemberMapper, times(2)).insert(any(ImGroupMemberDO.class)); } // ========== validateMemberInGroup ========== @Test public void testValidateMemberInGroup_notInGroup() { when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null); ServiceException exception = assertThrows(ServiceException.class, () -> groupMemberService.validateMemberInGroup(10L, 1L)); assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); } @Test public void testValidateMemberInGroup_disabledEqualsNotInGroup() { ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.DISABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); ServiceException exception = assertThrows(ServiceException.class, () -> groupMemberService.validateMemberInGroup(10L, 1L)); assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); } @Test public void testValidateMemberInGroup_success() { ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); ImGroupMemberDO result = groupMemberService.validateMemberInGroup(10L, 1L); assertEquals(50L, result.getId()); } // ========== updateGroupMember ========== @Test public void testUpdateGroupMember_success() { ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); ImGroupMemberUpdateReqVO reqVO = new ImGroupMemberUpdateReqVO() .setGroupId(10L).setSilent(true).setDisplayUserName("昵称"); groupMemberService.updateGroupMember(1L, reqVO); ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); verify(groupMemberMapper).updateById(captor.capture()); assertEquals(50L, captor.getValue().getId()); assertTrue(captor.getValue().getSilent()); // 公开字段昵称变化 → 全员在线同步 GROUP_MEMBER_NICKNAME_UPDATE verify(groupMessageService).sendGroupMessage(eq(1L), any(cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO.class)); // 个人字段 silent 变化 → 仅自己多端同步 GROUP_MEMBER_SETTING_UPDATE verify(groupMessageService).sendGroupMessage(eq(1L), eq(ListUtil.of(1L)), any(cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO.class)); } // ========== removeGroupMember ========== @Test public void testRemoveGroupMember_success() { ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); groupMemberService.removeGroupMember(10L, 1L); ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); verify(groupMemberMapper).updateById(captor.capture()); assertEquals(50L, captor.getValue().getId()); assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); assertNotNull(captor.getValue().getQuitTime()); } @Test public void testRemoveGroupMember_notInGroup() { when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null); ServiceException exception = assertThrows(ServiceException.class, () -> groupMemberService.removeGroupMember(10L, 1L)); assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); } // ========== removeGroupMembers ========== @Test public void testRemoveGroupMembers_batch() { groupMemberService.removeGroupMembers(10L, ListUtil.of(2L, 3L)); ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); verify(groupMemberMapper).updateByGroupIdAndUserIdsAndStatus(eq(10L), anyCollection(), eq(CommonStatusEnum.ENABLE.getStatus()), captor.capture()); assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); assertNotNull(captor.getValue().getQuitTime()); } @Test public void testRemoveGroupMembersByGroupId() { groupMemberService.removeGroupMembersByGroupId(10L); ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); verify(groupMemberMapper).updateByGroupIdAndStatus(eq(10L), eq(CommonStatusEnum.ENABLE.getStatus()), captor.capture()); assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); assertNotNull(captor.getValue().getQuitTime()); } // ========== getActiveGroupMemberUserIdsByGroupId ========== @Test public void testGetActiveGroupMemberUserIdsByGroupId_extractsUserIds() { // 准备:3 个 ENABLE 成员 List members = ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(), ImGroupMemberDO.builder().groupId(10L).userId(2L) .status(CommonStatusEnum.ENABLE.getStatus()).build(), ImGroupMemberDO.builder().groupId(10L).userId(3L) .status(CommonStatusEnum.ENABLE.getStatus()).build() ); when(groupMemberMapper.selectListByGroupIdAndStatus( 10L, CommonStatusEnum.ENABLE.getStatus())).thenReturn(members); // 调用 List userIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L); // 断言:只返回 userId、顺序保留 assertEquals(ListUtil.of(1L, 2L, 3L), userIds); } @Test public void testGetActiveGroupMemberUserIdsByGroupId_emptyList() { when(groupMemberMapper.selectListByGroupIdAndStatus( 10L, CommonStatusEnum.ENABLE.getStatus())).thenReturn(ListUtil.of()); List userIds = groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L); assertTrue(userIds.isEmpty()); } @Test public void testGetGroupMemberListByOwnerAndAdmin_passesRoles() { List roles = ListUtil.of(ImGroupMemberRoleEnum.OWNER.getRole(), ImGroupMemberRoleEnum.ADMIN.getRole()); List members = ListUtil.of(ImGroupMemberDO.builder().groupId(10L).userId(1L).build()); when(groupMemberMapper.selectListByGroupIdAndStatusAndRoles(10L, CommonStatusEnum.ENABLE.getStatus(), roles)) .thenReturn(members); List result = groupMemberService.getGroupMemberListByOwnerAndAdmin(10L); assertEquals(members, result); verify(groupMemberMapper).selectListByGroupIdAndStatusAndRoles( 10L, CommonStatusEnum.ENABLE.getStatus(), roles); } }