package cn.iocoder.yudao.module.im.service.group; import cn.hutool.core.collection.ListUtil; import cn.hutool.core.map.MapUtil; 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.request.ImGroupRequestApplyReqVO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupRequestDO; import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupRequestMapper; import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; import cn.iocoder.yudao.module.im.enums.group.ImGroupRequestHandleResultEnum; import cn.iocoder.yudao.module.im.enums.ImContentTypeEnum; import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; import cn.iocoder.yudao.module.system.api.user.AdminUserApi; import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; 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.Collection; import java.util.List; import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; import static org.junit.jupiter.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; /** * {@link ImGroupRequestServiceImpl} 的单元测试 * * @author 芋道源码 */ public class ImGroupRequestServiceImplTest extends BaseMockitoUnitTest { @InjectMocks private ImGroupRequestServiceImpl groupRequestService; @Mock private ImGroupRequestMapper groupRequestMapper; @Mock private ImGroupService groupService; @Mock private ImGroupMemberService groupMemberService; @Mock private ImGroupMessageService groupMessageService; @Mock private ImWebSocketService websocketService; @Mock private AdminUserApi adminUserApi; // ==================== applyJoinGroup ==================== @Test public void testApplyJoinGroup_freeMode_directJoin() { // 准备:群是 FREE 模式 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(false) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO(); reqVO.setGroupId(10L); reqVO.setAddSource(ImGroupAddSourceEnum.SEARCH.getSource()); // 调用 ImGroupRequestDO result = groupRequestService.applyJoinGroup(1L, reqVO); // 断言:FREE 路径直接入群,不落申请记录 assertNull(result); verify(groupService).validateMemberCountLimit(10L, 1); verify(groupMemberService).addGroupMember(eq(10L), eq(1L), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), eq(ImGroupAddSourceEnum.SEARCH.getSource()), isNull()); verify(groupRequestMapper, never()).insert(any(ImGroupRequestDO.class)); // 推 1510 自由进群 ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType()); } @Test public void testApplyJoinGroup_approvalMode_createsRequest() { // 准备:群是 APPLY 模式 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); // 群里有 owner + 一个 admin,作为 1503 推送目标 when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build(), ImGroupMemberDO.builder().groupId(10L).userId(98L) .role(ImGroupMemberRoleEnum.ADMIN.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build())); when(adminUserApi.getUser(1L)).thenReturn(buildUser(1L, "申请人")); ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO(); reqVO.setGroupId(10L); reqVO.setApplyContent("我想进群"); reqVO.setAddSource(ImGroupAddSourceEnum.SEARCH.getSource()); // 调用 ImGroupRequestDO result = groupRequestService.applyJoinGroup(1L, reqVO); // 断言:申请记录已落库 + 不直进群 assertNotNull(result); verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), anyInt(), anyLong()); verify(groupRequestMapper).insert(any(ImGroupRequestDO.class)); // 1503 推送给 owner(99) + admin(98),去重后两条 verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any()); } @Test public void testApplyJoinGroup_insertDuplicateKey_reuseOldRequest() { // 准备:群是 APPLY 模式,首次查询不存在,插入时命中唯一键 ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build())); when(adminUserApi.getUser(1L)).thenReturn(buildUser(1L, "申请人")); ImGroupRequestDO old = new ImGroupRequestDO().setId(50L).setGroupId(10L).setUserId(1L) .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()); when(groupRequestMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null, old); when(groupRequestMapper.insert(any(ImGroupRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO(); reqVO.setGroupId(10L); reqVO.setApplyContent("我想进群"); reqVO.setAddSource(ImGroupAddSourceEnum.SEARCH.getSource()); // 调用 ImGroupRequestDO result = groupRequestService.applyJoinGroup(1L, reqVO); // 断言:复用并重置旧申请 verify(groupRequestMapper).updateApplyByIdReset(eq(50L), eq("我想进群"), eq(ImGroupAddSourceEnum.SEARCH.getSource()), any()); assertEquals(50L, result.getId()); assertEquals(ImGroupRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult()); verify(websocketService).sendNotificationAsync(eq(99L), anyInt(), anyInt(), any()); } @Test public void testApplyJoinGroup_alreadyMember_throws() { ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); when(groupMemberService.getGroupMember(10L, 1L)).thenReturn( ImGroupMemberDO.builder().groupId(10L).userId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build()); ImGroupRequestApplyReqVO reqVO = new ImGroupRequestApplyReqVO(); reqVO.setGroupId(10L); ServiceException exception = assertThrows(ServiceException.class, () -> groupRequestService.applyJoinGroup(1L, reqVO)); assertEquals(GROUP_REQUEST_ALREADY_MEMBER.getCode(), exception.getCode()); } // ==================== agreeGroupRequest ==================== @Test public void testAgreeGroupRequest_success_activeApply() { // 准备:主动申请未处理,操作人是 admin ImGroupRequestDO request = new ImGroupRequestDO() .setGroupId(10L).setUserId(2L).setInviterUserId(null) .setAddSource(ImGroupAddSourceEnum.SEARCH.getSource()) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); request.setId(50L); when(groupRequestMapper.selectById(50L)).thenReturn(request); when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( ImGroupMemberDO.builder().groupId(10L).userId(1L) .role(ImGroupMemberRoleEnum.ADMIN.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build()); when(groupRequestMapper.updateByIdAndHandleResult(eq(50L), eq(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()), any())).thenReturn(1); ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.getGroup(10L)).thenReturn(group); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build())); // 调用 groupRequestService.agreeGroupRequest(1L, 50L); // 断言:人数校验 + 写群成员 + 推 1505 + 推 1510(主动申请) verify(groupService).validateMemberCountLimit(10L, 1); verify(groupMemberService).addGroupMember(eq(10L), eq(2L), eq(ImGroupMemberRoleEnum.NORMAL.getRole()), eq(ImGroupAddSourceEnum.SEARCH.getSource()), isNull()); ArgumentCaptor dtoCaptor = ArgumentCaptor.forClass(ImGroupMessageSendDTO.class); verify(groupMessageService).sendGroupMessage(eq(1L), dtoCaptor.capture()); assertEquals(ImContentTypeEnum.GROUP_MEMBER_ENTER.getType(), dtoCaptor.getValue().getType()); // 1505 推送给申请人 + owner,去重后两条 verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any()); } @Test public void testAgreeGroupRequest_concurrent_secondCallFails() { // 准备:申请存在但乐观锁更新返回 0(被并发处理过) ImGroupRequestDO request = new ImGroupRequestDO() .setGroupId(10L).setUserId(2L) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); request.setId(50L); when(groupRequestMapper.selectById(50L)).thenReturn(request); when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( ImGroupMemberDO.builder().groupId(10L).userId(1L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build()); when(groupRequestMapper.updateByIdAndHandleResult(eq(50L), eq(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()), any())).thenReturn(0); ServiceException exception = assertThrows(ServiceException.class, () -> groupRequestService.agreeGroupRequest(1L, 50L)); assertEquals(GROUP_REQUEST_HANDLED.getCode(), exception.getCode()); // 不写群成员 verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), any(), any()); } @Test public void testAgreeGroupRequest_notOwnerOrAdmin_throws() { ImGroupRequestDO request = new ImGroupRequestDO() .setGroupId(10L).setUserId(2L) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); request.setId(50L); when(groupRequestMapper.selectById(50L)).thenReturn(request); when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( ImGroupMemberDO.builder().groupId(10L).userId(1L) .role(ImGroupMemberRoleEnum.NORMAL.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build()); ServiceException exception = assertThrows(ServiceException.class, () -> groupRequestService.agreeGroupRequest(1L, 50L)); assertEquals(GROUP_REQUEST_NOT_TO_ME.getCode(), exception.getCode()); } // ==================== refuseGroupRequest ==================== @Test public void testRefuseGroupRequest_success() { ImGroupRequestDO request = new ImGroupRequestDO() .setGroupId(10L).setUserId(2L) .setHandleResult(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()); request.setId(50L); when(groupRequestMapper.selectById(50L)).thenReturn(request); when(groupMemberService.validateMemberInGroup(10L, 1L)).thenReturn( ImGroupMemberDO.builder().groupId(10L).userId(1L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build()); when(groupRequestMapper.updateByIdAndHandleResult(eq(50L), eq(ImGroupRequestHandleResultEnum.UNHANDLED.getResult()), any())).thenReturn(1); ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.getGroup(10L)).thenReturn(group); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(1L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build())); groupRequestService.refuseGroupRequest(1L, 50L, "暂不通过"); // 不写群成员;推 1506 给申请人 + 群主(同一人 1L 时去重为 1 + 申请人 2L = 2 条) verify(groupMemberService, never()).addGroupMember(anyLong(), anyLong(), anyInt(), any(), any()); verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any()); } // ==================== createInviteRequestList ==================== @Test public void testCreateInviteRequestList_success() { ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); when(groupRequestMapper.selectByGroupIdAndUserId(eq(10L), anyLong())).thenReturn(null); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build())); when(adminUserApi.getUserMap(anyCollection())).thenReturn(MapUtil.builder() .put(2L, buildUser(2L, "用户A")) .put(3L, buildUser(3L, "用户B")) .build()); // 调用:邀请人 1L 邀请 2L、3L(都没有旧记录) groupRequestService.createInviteRequestList(10L, 1L, ListUtil.of(2L, 3L)); // 断言:插入 2 条 + 推 1503 给 owner(每条 1 帧)共 2 帧 ArgumentCaptor captor = ArgumentCaptor.forClass(ImGroupRequestDO.class); verify(groupRequestMapper, times(2)).insert(captor.capture()); verify(websocketService, times(2)).sendNotificationAsync(anyLong(), anyInt(), anyInt(), any()); // 断言:每条记录 inviterUserId=1 + addSource=INVITE,避免审批通过后回写群成员留痕的来源为空 / 脏带旧值 Collection inserted = captor.getAllValues(); assertEquals(2, inserted.size()); inserted.forEach(insert -> { assertEquals(1L, insert.getInviterUserId()); assertEquals(ImGroupAddSourceEnum.INVITE.getSource(), insert.getAddSource()); }); } @Test public void testCreateInviteRequestList_insertDuplicateKey_reuseOldRequest() { ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) .joinApproval(true) .status(CommonStatusEnum.ENABLE.getStatus()).build(); when(groupService.validateGroupExists(10L)).thenReturn(group); ImGroupRequestDO old = new ImGroupRequestDO().setId(50L).setGroupId(10L).setUserId(2L) .setHandleResult(ImGroupRequestHandleResultEnum.REFUSED.getResult()); when(groupRequestMapper.selectByGroupIdAndUserId(10L, 2L)).thenReturn(null, old); when(groupRequestMapper.insert(any(ImGroupRequestDO.class))).thenThrow(new DuplicateKeyException("dup")); when(groupMemberService.getGroupMemberListByOwnerAndAdmin(10L)).thenReturn(ListUtil.of( ImGroupMemberDO.builder().groupId(10L).userId(99L) .role(ImGroupMemberRoleEnum.OWNER.getRole()) .status(CommonStatusEnum.ENABLE.getStatus()).build())); when(adminUserApi.getUserMap(anyCollection())).thenReturn(MapUtil.of(2L, buildUser(2L, "用户A"))); // 调用 groupRequestService.createInviteRequestList(10L, 1L, ListUtil.of(2L)); // 断言:复用并重置旧邀请申请 verify(groupRequestMapper).updateInviteByIdReset(eq(50L), eq(1L), eq(ImGroupAddSourceEnum.INVITE.getSource()), any()); verify(websocketService).sendNotificationAsync(eq(99L), anyInt(), anyInt(), any()); } private AdminUserRespDTO buildUser(Long id, String nickname) { AdminUserRespDTO user = new AdminUserRespDTO(); user.setId(id); user.setNickname(nickname); return user; } }