Commit 16d5c0b0cc5dba7d899f94700543daf1de5bdddd
1 parent
843cc90b
✨ feat(im): 完善单元测试
Showing
12 changed files
with
1926 additions
and
258 deletions
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapperTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.group; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | ||
| 5 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 6 | +import jakarta.annotation.Resource; | ||
| 7 | +import org.junit.jupiter.api.Test; | ||
| 8 | + | ||
| 9 | +import java.time.LocalDateTime; | ||
| 10 | +import java.util.List; | ||
| 11 | + | ||
| 12 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * IM 群成员 Mapper 单元测试 | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +public class ImGroupMemberMapperTest extends BaseDbUnitTest { | ||
| 20 | + | ||
| 21 | + @Resource | ||
| 22 | + private ImGroupMemberMapper mapper; | ||
| 23 | + | ||
| 24 | + // ========== selectListByGroupIdAndUserIds ========== | ||
| 25 | + | ||
| 26 | + @Test | ||
| 27 | + public void testSelectListByGroupIdAndUserIds() { | ||
| 28 | + // 准备:同一群 3 个成员 + 另一群 1 个成员 | ||
| 29 | + ImGroupMemberDO u1 = buildMember(10L, 1L, CommonStatusEnum.ENABLE); | ||
| 30 | + mapper.insert(u1); | ||
| 31 | + ImGroupMemberDO u2 = buildMember(10L, 2L, CommonStatusEnum.ENABLE); | ||
| 32 | + mapper.insert(u2); | ||
| 33 | + ImGroupMemberDO u3 = buildMember(10L, 3L, CommonStatusEnum.DISABLE); | ||
| 34 | + mapper.insert(u3); | ||
| 35 | + // 不相关的另一个群 | ||
| 36 | + mapper.insert(buildMember(20L, 1L, CommonStatusEnum.ENABLE)); | ||
| 37 | + | ||
| 38 | + List<ImGroupMemberDO> result = mapper.selectListByGroupIdAndUserIds(10L, List.of(1L, 2L, 3L)); | ||
| 39 | + assertEquals(3, result.size()); | ||
| 40 | + } | ||
| 41 | + | ||
| 42 | + @Test | ||
| 43 | + public void testSelectListByGroupIdAndUserIds_mixedUserIds() { | ||
| 44 | + mapper.insert(buildMember(10L, 1L, CommonStatusEnum.ENABLE)); | ||
| 45 | + ImGroupMemberDO u2 = buildMember(10L, 2L, CommonStatusEnum.ENABLE); | ||
| 46 | + mapper.insert(u2); | ||
| 47 | + | ||
| 48 | + // 查询不存在的用户 99,返回为空 | ||
| 49 | + List<ImGroupMemberDO> result = mapper.selectListByGroupIdAndUserIds(10L, List.of(99L)); | ||
| 50 | + assertTrue(result.isEmpty()); | ||
| 51 | + | ||
| 52 | + // 查询包含存在和不存在混合,只返回存在的 | ||
| 53 | + List<ImGroupMemberDO> result2 = mapper.selectListByGroupIdAndUserIds(10L, List.of(2L, 99L)); | ||
| 54 | + assertEquals(1, result2.size()); | ||
| 55 | + assertEquals(u2.getId(), result2.get(0).getId()); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // ========== selectListByGroupIdAndStatus ========== | ||
| 59 | + | ||
| 60 | + @Test | ||
| 61 | + public void testSelectListByGroupIdAndStatus_onlyMatchingStatus() { | ||
| 62 | + mapper.insert(buildMember(10L, 1L, CommonStatusEnum.ENABLE)); | ||
| 63 | + mapper.insert(buildMember(10L, 2L, CommonStatusEnum.ENABLE)); | ||
| 64 | + mapper.insert(buildMember(10L, 3L, CommonStatusEnum.DISABLE)); | ||
| 65 | + | ||
| 66 | + List<ImGroupMemberDO> active = mapper.selectListByGroupIdAndStatus( | ||
| 67 | + 10L, CommonStatusEnum.ENABLE.getStatus()); | ||
| 68 | + assertEquals(2, active.size()); | ||
| 69 | + | ||
| 70 | + List<ImGroupMemberDO> disabled = mapper.selectListByGroupIdAndStatus( | ||
| 71 | + 10L, CommonStatusEnum.DISABLE.getStatus()); | ||
| 72 | + assertEquals(1, disabled.size()); | ||
| 73 | + } | ||
| 74 | + | ||
| 75 | + // ========== selectListByUserIdAndStatus ========== | ||
| 76 | + | ||
| 77 | + @Test | ||
| 78 | + public void testSelectListByUserIdAndStatus() { | ||
| 79 | + // 用户 1 在群 10、20 都有记录;在群 30 已退群 | ||
| 80 | + mapper.insert(buildMember(10L, 1L, CommonStatusEnum.ENABLE)); | ||
| 81 | + mapper.insert(buildMember(20L, 1L, CommonStatusEnum.ENABLE)); | ||
| 82 | + mapper.insert(buildMember(30L, 1L, CommonStatusEnum.DISABLE)); | ||
| 83 | + // 另一个用户的记录 | ||
| 84 | + mapper.insert(buildMember(10L, 2L, CommonStatusEnum.ENABLE)); | ||
| 85 | + | ||
| 86 | + List<ImGroupMemberDO> active = mapper.selectListByUserIdAndStatus( | ||
| 87 | + 1L, CommonStatusEnum.ENABLE.getStatus()); | ||
| 88 | + assertEquals(2, active.size()); | ||
| 89 | + assertTrue(active.stream().allMatch(m -> m.getUserId().equals(1L))); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + // ========== selectQuitListByUserId ========== | ||
| 93 | + | ||
| 94 | + @Test | ||
| 95 | + public void testSelectQuitListByUserId_onlyDisabled() { | ||
| 96 | + // 准备:一条 ENABLE + 一条 DISABLE | ||
| 97 | + mapper.insert(buildMember(10L, 1L, CommonStatusEnum.ENABLE)); | ||
| 98 | + ImGroupMemberDO quit = buildMember(20L, 1L, CommonStatusEnum.DISABLE); | ||
| 99 | + quit.setQuitTime(LocalDateTime.now().minusDays(1)); | ||
| 100 | + mapper.insert(quit); | ||
| 101 | + | ||
| 102 | + // 调用:minQuitTime 为 null,不按时间过滤 | ||
| 103 | + List<ImGroupMemberDO> result = mapper.selectQuitListByUserId(1L, null); | ||
| 104 | + | ||
| 105 | + // 断言:只返回退群记录 | ||
| 106 | + assertEquals(1, result.size()); | ||
| 107 | + assertEquals(quit.getId(), result.get(0).getId()); | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + @Test | ||
| 111 | + public void testSelectQuitListByUserId_filterByMinQuitTime() { | ||
| 112 | + // 准备:两条退群记录,时间一早一晚 | ||
| 113 | + LocalDateTime old = LocalDateTime.now().minusDays(10); | ||
| 114 | + LocalDateTime recent = LocalDateTime.now().minusDays(1); | ||
| 115 | + ImGroupMemberDO oldQuit = buildMember(10L, 1L, CommonStatusEnum.DISABLE); | ||
| 116 | + oldQuit.setQuitTime(old); | ||
| 117 | + mapper.insert(oldQuit); | ||
| 118 | + ImGroupMemberDO recentQuit = buildMember(20L, 1L, CommonStatusEnum.DISABLE); | ||
| 119 | + recentQuit.setQuitTime(recent); | ||
| 120 | + mapper.insert(recentQuit); | ||
| 121 | + | ||
| 122 | + // 调用:minQuitTime = now - 5 天,只返回近期退群 | ||
| 123 | + List<ImGroupMemberDO> result = mapper.selectQuitListByUserId(1L, LocalDateTime.now().minusDays(5)); | ||
| 124 | + assertEquals(1, result.size()); | ||
| 125 | + assertEquals(recentQuit.getId(), result.get(0).getId()); | ||
| 126 | + | ||
| 127 | + // 调用:minQuitTime 为 null,不过滤 | ||
| 128 | + List<ImGroupMemberDO> all = mapper.selectQuitListByUserId(1L, null); | ||
| 129 | + assertEquals(2, all.size()); | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + // ========== updateByGroupIdAndUserIdsAndStatus ========== | ||
| 133 | + | ||
| 134 | + @Test | ||
| 135 | + public void testUpdateByGroupIdAndUserIdsAndStatus_onlyUpdatesOldStatusMatches() { | ||
| 136 | + // 准备:u1 ENABLE,u2 DISABLE(已退群,不应更新) | ||
| 137 | + ImGroupMemberDO u1 = buildMember(10L, 1L, CommonStatusEnum.ENABLE); | ||
| 138 | + mapper.insert(u1); | ||
| 139 | + ImGroupMemberDO u2 = buildMember(10L, 2L, CommonStatusEnum.DISABLE); | ||
| 140 | + u2.setQuitTime(LocalDateTime.now().minusDays(5)); | ||
| 141 | + mapper.insert(u2); | ||
| 142 | + | ||
| 143 | + // 调用:把 ENABLE 状态的成员移除 | ||
| 144 | + LocalDateTime now = LocalDateTime.now(); | ||
| 145 | + mapper.updateByGroupIdAndUserIdsAndStatus(10L, List.of(1L, 2L), | ||
| 146 | + CommonStatusEnum.ENABLE.getStatus(), | ||
| 147 | + new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus()).setQuitTime(now)); | ||
| 148 | + | ||
| 149 | + // 断言:u1 被更新 | ||
| 150 | + ImGroupMemberDO after1 = mapper.selectById(u1.getId()); | ||
| 151 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), after1.getStatus()); | ||
| 152 | + assertNotNull(after1.getQuitTime()); | ||
| 153 | + // 断言:u2 的 quitTime 不变(仍为 5 天前) | ||
| 154 | + ImGroupMemberDO after2 = mapper.selectById(u2.getId()); | ||
| 155 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), after2.getStatus()); | ||
| 156 | + assertTrue(after2.getQuitTime().isBefore(now.minusDays(1))); | ||
| 157 | + } | ||
| 158 | + | ||
| 159 | + // ========== updateByGroupIdAndStatus ========== | ||
| 160 | + | ||
| 161 | + @Test | ||
| 162 | + public void testUpdateByGroupIdAndStatus_allActiveInGroup() { | ||
| 163 | + // 准备:群 10 有 2 个活跃 + 1 个已退;群 20 有 1 个活跃 | ||
| 164 | + ImGroupMemberDO a1 = buildMember(10L, 1L, CommonStatusEnum.ENABLE); | ||
| 165 | + mapper.insert(a1); | ||
| 166 | + ImGroupMemberDO a2 = buildMember(10L, 2L, CommonStatusEnum.ENABLE); | ||
| 167 | + mapper.insert(a2); | ||
| 168 | + ImGroupMemberDO a3 = buildMember(10L, 3L, CommonStatusEnum.DISABLE); | ||
| 169 | + a3.setQuitTime(LocalDateTime.now().minusDays(5)); | ||
| 170 | + mapper.insert(a3); | ||
| 171 | + ImGroupMemberDO b1 = buildMember(20L, 1L, CommonStatusEnum.ENABLE); | ||
| 172 | + mapper.insert(b1); | ||
| 173 | + | ||
| 174 | + // 调用:解散群 10 | ||
| 175 | + mapper.updateByGroupIdAndStatus(10L, CommonStatusEnum.ENABLE.getStatus(), | ||
| 176 | + new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus()) | ||
| 177 | + .setQuitTime(LocalDateTime.now())); | ||
| 178 | + | ||
| 179 | + // 断言:群 10 的活跃成员全部变为 DISABLE,群 20 的不受影响 | ||
| 180 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), mapper.selectById(a1.getId()).getStatus()); | ||
| 181 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), mapper.selectById(a2.getId()).getStatus()); | ||
| 182 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), mapper.selectById(b1.getId()).getStatus()); | ||
| 183 | + } | ||
| 184 | + | ||
| 185 | + // ========== 工具方法 ========== | ||
| 186 | + | ||
| 187 | + private ImGroupMemberDO buildMember(Long groupId, Long userId, CommonStatusEnum status) { | ||
| 188 | + return ImGroupMemberDO.builder() | ||
| 189 | + .groupId(groupId).userId(userId) | ||
| 190 | + .status(status.getStatus()) | ||
| 191 | + .joinTime(LocalDateTime.now().minusDays(30)) | ||
| 192 | + .build(); | ||
| 193 | + } | ||
| 194 | + | ||
| 195 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.message; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 5 | +import cn.iocoder.yudao.module.im.enums.message.ImGroupMessageReceiptStatusEnum; | ||
| 6 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 7 | +import jakarta.annotation.Resource; | ||
| 8 | +import org.junit.jupiter.api.Test; | ||
| 9 | + | ||
| 10 | +import java.time.LocalDateTime; | ||
| 11 | +import java.util.List; | ||
| 12 | + | ||
| 13 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 14 | + | ||
| 15 | +/** | ||
| 16 | + * IM 群聊消息 Mapper 单元测试 | ||
| 17 | + * | ||
| 18 | + * @author 芋道源码 | ||
| 19 | + */ | ||
| 20 | +public class ImGroupMessageMapperTest extends BaseDbUnitTest { | ||
| 21 | + | ||
| 22 | + @Resource | ||
| 23 | + private ImGroupMessageMapper mapper; | ||
| 24 | + | ||
| 25 | + private static final LocalDateTime FAR_PAST = LocalDateTime.of(2000, 1, 1, 0, 0, 0); | ||
| 26 | + | ||
| 27 | + // ========== selectListByMinId ========== | ||
| 28 | + | ||
| 29 | + @Test | ||
| 30 | + public void testSelectListByMinId_onlyReturnsGroupsInIdList() { | ||
| 31 | + // 准备:群 10 的消息 + 群 20 的消息 | ||
| 32 | + ImGroupMessageDO msg10 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 33 | + mapper.insert(msg10); | ||
| 34 | + ImGroupMessageDO msg20 = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 35 | + mapper.insert(msg20); | ||
| 36 | + | ||
| 37 | + // 调用:只查群 10 | ||
| 38 | + List<ImGroupMessageDO> result = mapper.selectListByMinId(List.of(10L), 0L, FAR_PAST, 100); | ||
| 39 | + | ||
| 40 | + // 断言:只有 msg10 | ||
| 41 | + assertEquals(1, result.size()); | ||
| 42 | + assertEquals(msg10.getId(), result.get(0).getId()); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @Test | ||
| 46 | + public void testSelectListByMinId_excludeRecall() { | ||
| 47 | + // 准备:一条正常 + 一条撤回 | ||
| 48 | + ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 49 | + mapper.insert(normal); | ||
| 50 | + ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); | ||
| 51 | + mapper.insert(recalled); | ||
| 52 | + | ||
| 53 | + List<ImGroupMessageDO> result = mapper.selectListByMinId(List.of(10L), 0L, FAR_PAST, 100); | ||
| 54 | + | ||
| 55 | + assertEquals(1, result.size()); | ||
| 56 | + assertEquals(normal.getId(), result.get(0).getId()); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + @Test | ||
| 60 | + public void testSelectListByMinId_sendTimeWindow() { | ||
| 61 | + // 准备:一条在窗口内,一条在窗口外 | ||
| 62 | + ImGroupMessageDO inWindow = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 63 | + inWindow.setSendTime(LocalDateTime.now().minusDays(1)); | ||
| 64 | + mapper.insert(inWindow); | ||
| 65 | + ImGroupMessageDO outWindow = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 66 | + outWindow.setSendTime(LocalDateTime.now().minusDays(40)); | ||
| 67 | + mapper.insert(outWindow); | ||
| 68 | + | ||
| 69 | + List<ImGroupMessageDO> result = mapper.selectListByMinId(List.of(10L), 0L, | ||
| 70 | + LocalDateTime.now().minusDays(30), 100); | ||
| 71 | + | ||
| 72 | + assertEquals(1, result.size()); | ||
| 73 | + assertEquals(inWindow.getId(), result.get(0).getId()); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @Test | ||
| 77 | + public void testSelectListByMinId_sortAscLimit() { | ||
| 78 | + // 准备:插入 3 条 | ||
| 79 | + ImGroupMessageDO m1 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 80 | + mapper.insert(m1); | ||
| 81 | + ImGroupMessageDO m2 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 82 | + mapper.insert(m2); | ||
| 83 | + ImGroupMessageDO m3 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 84 | + mapper.insert(m3); | ||
| 85 | + | ||
| 86 | + // 调用:size=2,返回 id 最小的 2 条 | ||
| 87 | + List<ImGroupMessageDO> result = mapper.selectListByMinId(List.of(10L), 0L, FAR_PAST, 2); | ||
| 88 | + | ||
| 89 | + assertEquals(2, result.size()); | ||
| 90 | + assertTrue(result.get(0).getId() < result.get(1).getId()); | ||
| 91 | + assertEquals(m1.getId(), result.get(0).getId()); | ||
| 92 | + assertEquals(m2.getId(), result.get(1).getId()); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + // ========== selectListByGroupIdAndMinIdAndQuitTimeBefore ========== | ||
| 96 | + | ||
| 97 | + @Test | ||
| 98 | + public void testSelectListByGroupIdAndMinIdAndQuitTimeBefore_onlyReturnsBeforeQuit() { | ||
| 99 | + // 准备:退群时间 = now-1 day,插入一条"退群前",一条"退群后" | ||
| 100 | + LocalDateTime quitTime = LocalDateTime.now().minusDays(1); | ||
| 101 | + ImGroupMessageDO before = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 102 | + before.setSendTime(LocalDateTime.now().minusDays(2)); | ||
| 103 | + mapper.insert(before); | ||
| 104 | + ImGroupMessageDO after = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 105 | + after.setSendTime(LocalDateTime.now().minusHours(1)); | ||
| 106 | + mapper.insert(after); | ||
| 107 | + | ||
| 108 | + List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndMinIdAndQuitTimeBefore( | ||
| 109 | + 10L, 0L, FAR_PAST, quitTime, 100); | ||
| 110 | + | ||
| 111 | + // 断言:只返回退群前消息 | ||
| 112 | + assertEquals(1, result.size()); | ||
| 113 | + assertEquals(before.getId(), result.get(0).getId()); | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + @Test | ||
| 117 | + public void testSelectListByGroupIdAndMinIdAndQuitTimeBefore_boundaryEqualsQuitTime() { | ||
| 118 | + // 准备:消息发送时间恰好等于退群时间(le 语义应包含该条) | ||
| 119 | + LocalDateTime quitTime = LocalDateTime.now().minusDays(1).withNano(0); | ||
| 120 | + ImGroupMessageDO boundary = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 121 | + boundary.setSendTime(quitTime); | ||
| 122 | + mapper.insert(boundary); | ||
| 123 | + | ||
| 124 | + List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndMinIdAndQuitTimeBefore( | ||
| 125 | + 10L, 0L, FAR_PAST, quitTime, 100); | ||
| 126 | + | ||
| 127 | + assertEquals(1, result.size()); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @Test | ||
| 131 | + public void testSelectListByGroupIdAndMinIdAndQuitTimeBefore_otherGroupExcluded() { | ||
| 132 | + // 准备:群 10 和群 20 各一条消息 | ||
| 133 | + LocalDateTime quitTime = LocalDateTime.now(); | ||
| 134 | + ImGroupMessageDO in10 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 135 | + in10.setSendTime(LocalDateTime.now().minusHours(2)); | ||
| 136 | + mapper.insert(in10); | ||
| 137 | + ImGroupMessageDO in20 = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 138 | + in20.setSendTime(LocalDateTime.now().minusHours(2)); | ||
| 139 | + mapper.insert(in20); | ||
| 140 | + | ||
| 141 | + List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndMinIdAndQuitTimeBefore( | ||
| 142 | + 10L, 0L, FAR_PAST, quitTime, 100); | ||
| 143 | + | ||
| 144 | + assertEquals(1, result.size()); | ||
| 145 | + assertEquals(in10.getId(), result.get(0).getId()); | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + // ========== selectHistoryList ========== | ||
| 149 | + | ||
| 150 | + @Test | ||
| 151 | + public void testSelectHistoryList_basic() { | ||
| 152 | + ImGroupMessageDO m1 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 153 | + mapper.insert(m1); | ||
| 154 | + ImGroupMessageDO m2 = buildMessage(10L, 2L, ImMessageStatusEnum.UNREAD); | ||
| 155 | + mapper.insert(m2); | ||
| 156 | + // 别的群 | ||
| 157 | + ImGroupMessageDO other = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 158 | + mapper.insert(other); | ||
| 159 | + | ||
| 160 | + List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, null, 100, null); | ||
| 161 | + | ||
| 162 | + // 断言:只返回 10 群的,按 id 倒序 | ||
| 163 | + assertEquals(2, result.size()); | ||
| 164 | + assertTrue(result.get(0).getId() > result.get(1).getId()); | ||
| 165 | + } | ||
| 166 | + | ||
| 167 | + @Test | ||
| 168 | + public void testSelectHistoryList_excludeRecall() { | ||
| 169 | + ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 170 | + mapper.insert(normal); | ||
| 171 | + ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); | ||
| 172 | + mapper.insert(recalled); | ||
| 173 | + | ||
| 174 | + List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, null, 100, null); | ||
| 175 | + | ||
| 176 | + assertEquals(1, result.size()); | ||
| 177 | + assertEquals(normal.getId(), result.get(0).getId()); | ||
| 178 | + } | ||
| 179 | + | ||
| 180 | + @Test | ||
| 181 | + public void testSelectHistoryList_maxIdCursor() { | ||
| 182 | + ImGroupMessageDO m1 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 183 | + mapper.insert(m1); | ||
| 184 | + ImGroupMessageDO m2 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 185 | + mapper.insert(m2); | ||
| 186 | + ImGroupMessageDO m3 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 187 | + mapper.insert(m3); | ||
| 188 | + | ||
| 189 | + List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, m3.getId(), 100, null); | ||
| 190 | + | ||
| 191 | + // 断言:只返回 id < m3.id 的,即 m1、m2 | ||
| 192 | + assertEquals(2, result.size()); | ||
| 193 | + assertTrue(result.stream().allMatch(m -> m.getId() < m3.getId())); | ||
| 194 | + } | ||
| 195 | + | ||
| 196 | + @Test | ||
| 197 | + public void testSelectHistoryList_filterByJoinTime() { | ||
| 198 | + // 准备:一条在入群前,一条在入群后 | ||
| 199 | + LocalDateTime joinTime = LocalDateTime.now().minusHours(1); | ||
| 200 | + ImGroupMessageDO before = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 201 | + before.setSendTime(LocalDateTime.now().minusHours(2)); | ||
| 202 | + mapper.insert(before); | ||
| 203 | + ImGroupMessageDO after = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 204 | + after.setSendTime(LocalDateTime.now().minusMinutes(30)); | ||
| 205 | + mapper.insert(after); | ||
| 206 | + | ||
| 207 | + List<ImGroupMessageDO> result = mapper.selectHistoryList(10L, null, 100, joinTime); | ||
| 208 | + | ||
| 209 | + // 断言:入群前消息不可见 | ||
| 210 | + assertEquals(1, result.size()); | ||
| 211 | + assertEquals(after.getId(), result.get(0).getId()); | ||
| 212 | + } | ||
| 213 | + | ||
| 214 | + // ========== selectBySenderIdAndClientMessageId ========== | ||
| 215 | + | ||
| 216 | + @Test | ||
| 217 | + public void testSelectBySenderIdAndClientMessageId() { | ||
| 218 | + ImGroupMessageDO msg = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 219 | + msg.setClientMessageId("group-uuid-001"); | ||
| 220 | + mapper.insert(msg); | ||
| 221 | + | ||
| 222 | + ImGroupMessageDO result = mapper.selectBySenderIdAndClientMessageId(1L, "group-uuid-001"); | ||
| 223 | + assertNotNull(result); | ||
| 224 | + assertEquals(msg.getId(), result.getId()); | ||
| 225 | + | ||
| 226 | + assertNull(mapper.selectBySenderIdAndClientMessageId(1L, "not-exist")); | ||
| 227 | + } | ||
| 228 | + | ||
| 229 | + // ========== selectListByGroupIdAndPendingReceipt ========== | ||
| 230 | + | ||
| 231 | + @Test | ||
| 232 | + public void testSelectListByGroupIdAndPendingReceipt_basic() { | ||
| 233 | + // 准备:待回执 + 已完成 + 无需回执,三种状态 | ||
| 234 | + ImGroupMessageDO pending = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 235 | + pending.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 236 | + mapper.insert(pending); | ||
| 237 | + ImGroupMessageDO done = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 238 | + done.setReceiptStatus(ImGroupMessageReceiptStatusEnum.DONE.getStatus()); | ||
| 239 | + mapper.insert(done); | ||
| 240 | + ImGroupMessageDO noReceipt = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 241 | + noReceipt.setReceiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()); | ||
| 242 | + mapper.insert(noReceipt); | ||
| 243 | + | ||
| 244 | + List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndPendingReceipt(10L, null, Long.MAX_VALUE); | ||
| 245 | + | ||
| 246 | + // 断言:只返回 pending 记录 | ||
| 247 | + assertEquals(1, result.size()); | ||
| 248 | + assertEquals(pending.getId(), result.get(0).getId()); | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | + @Test | ||
| 252 | + public void testSelectListByGroupIdAndPendingReceipt_windowAndRecall() { | ||
| 253 | + // 准备:窗口外 + 撤回 + 窗口内 pending | ||
| 254 | + ImGroupMessageDO belowRange = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 255 | + belowRange.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 256 | + mapper.insert(belowRange); | ||
| 257 | + ImGroupMessageDO inRange = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 258 | + inRange.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 259 | + mapper.insert(inRange); | ||
| 260 | + ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); | ||
| 261 | + recalled.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 262 | + mapper.insert(recalled); | ||
| 263 | + | ||
| 264 | + // 调用:minId = belowRange.id(排除), maxId = inRange.id(包含) | ||
| 265 | + List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndPendingReceipt( | ||
| 266 | + 10L, belowRange.getId(), inRange.getId()); | ||
| 267 | + | ||
| 268 | + // 断言:只返回窗口内的非撤回 pending,即 inRange | ||
| 269 | + assertEquals(1, result.size()); | ||
| 270 | + assertEquals(inRange.getId(), result.get(0).getId()); | ||
| 271 | + } | ||
| 272 | + | ||
| 273 | + @Test | ||
| 274 | + public void testSelectListByGroupIdAndPendingReceipt_otherGroupExcluded() { | ||
| 275 | + ImGroupMessageDO g10 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 276 | + g10.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 277 | + mapper.insert(g10); | ||
| 278 | + ImGroupMessageDO g20 = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 279 | + g20.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 280 | + mapper.insert(g20); | ||
| 281 | + | ||
| 282 | + List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndPendingReceipt(10L, null, Long.MAX_VALUE); | ||
| 283 | + assertEquals(1, result.size()); | ||
| 284 | + assertEquals(g10.getId(), result.get(0).getId()); | ||
| 285 | + } | ||
| 286 | + | ||
| 287 | + // ========== 工具方法 ========== | ||
| 288 | + | ||
| 289 | + private ImGroupMessageDO buildMessage(Long groupId, Long senderId, ImMessageStatusEnum status) { | ||
| 290 | + return ImGroupMessageDO.builder() | ||
| 291 | + .clientMessageId("uuid-" + System.nanoTime()) | ||
| 292 | + .senderId(senderId) | ||
| 293 | + .groupId(groupId) | ||
| 294 | + .type(0) | ||
| 295 | + .content("{\"content\":\"test\"}") | ||
| 296 | + .status(status.getStatus()) | ||
| 297 | + .sendTime(LocalDateTime.now()) | ||
| 298 | + .receiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()) | ||
| 299 | + .build(); | ||
| 300 | + } | ||
| 301 | + | ||
| 302 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.friend; | ||
| 2 | + | ||
| 3 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 5 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 6 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.friend.vo.ImFriendUpdateReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendMapper; | ||
| 10 | +import cn.iocoder.yudao.module.im.service.message.ImPrivateMessageService; | ||
| 11 | +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | ||
| 12 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | ||
| 13 | +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; | ||
| 14 | +import org.junit.jupiter.api.Test; | ||
| 15 | +import org.mockito.ArgumentCaptor; | ||
| 16 | +import org.mockito.InjectMocks; | ||
| 17 | +import org.mockito.Mock; | ||
| 18 | +import org.mockito.MockedStatic; | ||
| 19 | +import org.springframework.dao.DuplicateKeyException; | ||
| 20 | + | ||
| 21 | +import java.time.LocalDateTime; | ||
| 22 | +import java.util.List; | ||
| 23 | + | ||
| 24 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_ADD_SELF; | ||
| 25 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; | ||
| 26 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 27 | +import static org.mockito.ArgumentMatchers.any; | ||
| 28 | +import static org.mockito.ArgumentMatchers.anyString; | ||
| 29 | +import static org.mockito.ArgumentMatchers.eq; | ||
| 30 | +import static org.mockito.Mockito.mockStatic; | ||
| 31 | +import static org.mockito.Mockito.never; | ||
| 32 | +import static org.mockito.Mockito.times; | ||
| 33 | +import static org.mockito.Mockito.verify; | ||
| 34 | +import static org.mockito.Mockito.when; | ||
| 35 | + | ||
| 36 | +/** | ||
| 37 | + * IM 好友关系 Service 单元测试 | ||
| 38 | + * | ||
| 39 | + * @author 芋道源码 | ||
| 40 | + */ | ||
| 41 | +public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 42 | + | ||
| 43 | + @InjectMocks | ||
| 44 | + private ImFriendServiceImpl friendService; | ||
| 45 | + | ||
| 46 | + @Mock | ||
| 47 | + private ImFriendMapper imFriendMapper; | ||
| 48 | + @Mock | ||
| 49 | + private AdminUserApi adminUserApi; | ||
| 50 | + @Mock | ||
| 51 | + private ImWebSocketService imWebSocketService; | ||
| 52 | + @Mock | ||
| 53 | + private ImPrivateMessageService privateMessageService; | ||
| 54 | + | ||
| 55 | + // ========== isFriend ========== | ||
| 56 | + | ||
| 57 | + @Test | ||
| 58 | + public void testIsFriend_enabled() { | ||
| 59 | + ImFriendDO friend = ImFriendDO.builder().userId(1L).friendUserId(2L) | ||
| 60 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 61 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(friend); | ||
| 62 | + | ||
| 63 | + assertTrue(friendService.isFriend(1L, 2L)); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + @Test | ||
| 67 | + public void testIsFriend_disabledOrAbsent() { | ||
| 68 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(null); | ||
| 69 | + assertFalse(friendService.isFriend(1L, 2L)); | ||
| 70 | + | ||
| 71 | + ImFriendDO disabled = ImFriendDO.builder().userId(1L).friendUserId(3L) | ||
| 72 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 73 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 3L)).thenReturn(disabled); | ||
| 74 | + assertFalse(friendService.isFriend(1L, 3L)); | ||
| 75 | + } | ||
| 76 | + | ||
| 77 | + // ========== updateFriend ========== | ||
| 78 | + | ||
| 79 | + @Test | ||
| 80 | + public void testUpdateFriend_success() { | ||
| 81 | + // 准备 | ||
| 82 | + ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); | ||
| 83 | + reqVO.setFriendUserId(2L); | ||
| 84 | + reqVO.setMuted(true); | ||
| 85 | + ImFriendDO friend = ImFriendDO.builder().id(100L).userId(1L).friendUserId(2L) | ||
| 86 | + .muted(false).status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 87 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(friend); | ||
| 88 | + | ||
| 89 | + // 调用 | ||
| 90 | + friendService.updateFriend(1L, reqVO); | ||
| 91 | + | ||
| 92 | + // 断言:更新了 muted 字段 | ||
| 93 | + ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); | ||
| 94 | + verify(imFriendMapper).updateById(captor.capture()); | ||
| 95 | + assertEquals(100L, captor.getValue().getId()); | ||
| 96 | + assertTrue(captor.getValue().getMuted()); | ||
| 97 | + // 断言:推送了好友更新通知 | ||
| 98 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + @Test | ||
| 102 | + public void testUpdateFriend_notFriend() { | ||
| 103 | + // 准备 | ||
| 104 | + ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); | ||
| 105 | + reqVO.setFriendUserId(2L); | ||
| 106 | + reqVO.setMuted(true); | ||
| 107 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(null); | ||
| 108 | + | ||
| 109 | + // 调用并断言 | ||
| 110 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 111 | + () -> friendService.updateFriend(1L, reqVO)); | ||
| 112 | + assertEquals(FRIEND_NOT_FRIEND.getCode(), exception.getCode()); | ||
| 113 | + } | ||
| 114 | + | ||
| 115 | + // ========== addFriend ========== | ||
| 116 | + | ||
| 117 | + @Test | ||
| 118 | + public void testAddFriend_self() { | ||
| 119 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 120 | + () -> friendService.addFriend(1L, 1L)); | ||
| 121 | + assertEquals(FRIEND_ADD_SELF.getCode(), exception.getCode()); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + @Test | ||
| 125 | + public void testAddFriend_success() { | ||
| 126 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 127 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImFriendServiceImpl.class))) | ||
| 128 | + .thenReturn(friendService); | ||
| 129 | + | ||
| 130 | + // 准备 | ||
| 131 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(null); | ||
| 132 | + when(imFriendMapper.selectByUserIdAndFriendUserId(2L, 1L)).thenReturn(null); | ||
| 133 | + | ||
| 134 | + // 调用 | ||
| 135 | + friendService.addFriend(1L, 2L); | ||
| 136 | + | ||
| 137 | + // 断言:校验对方存在 | ||
| 138 | + verify(adminUserApi).validateUser(2L); | ||
| 139 | + // 断言:双向插入 2 条好友关系 | ||
| 140 | + verify(imFriendMapper, times(2)).insert(any(ImFriendDO.class)); | ||
| 141 | + // 断言:发送提示消息 + 给双方各推送一次好友添加事件 | ||
| 142 | + verify(privateMessageService).sendTipPrivateMessage(eq(1L), eq(2L), anyString()); | ||
| 143 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | ||
| 144 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); | ||
| 145 | + } | ||
| 146 | + } | ||
| 147 | + | ||
| 148 | + @Test | ||
| 149 | + public void testAddFriend0_existingEnabledSkipsUpdate() { | ||
| 150 | + // 准备:已存在且启用,第二步不应触发更新 | ||
| 151 | + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | ||
| 152 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 153 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); | ||
| 154 | + | ||
| 155 | + // 调用 | ||
| 156 | + friendService.addFriend0(1L, 2L); | ||
| 157 | + | ||
| 158 | + // 断言:不插入,也不更新 | ||
| 159 | + verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); | ||
| 160 | + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); | ||
| 161 | + } | ||
| 162 | + | ||
| 163 | + @Test | ||
| 164 | + public void testAddFriend0_existingDisabledRecovers() { | ||
| 165 | + // 准备:已存在且 DISABLE,应当恢复状态 | ||
| 166 | + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | ||
| 167 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 168 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); | ||
| 169 | + | ||
| 170 | + // 调用 | ||
| 171 | + friendService.addFriend0(1L, 2L); | ||
| 172 | + | ||
| 173 | + // 断言:更新 status 为 ENABLE | ||
| 174 | + ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); | ||
| 175 | + verify(imFriendMapper).updateById(captor.capture()); | ||
| 176 | + assertEquals(10L, captor.getValue().getId()); | ||
| 177 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | ||
| 178 | + verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + @Test | ||
| 182 | + public void testAddFriend0_duplicateKeySwallowed() { | ||
| 183 | + // 准备:mapper 抛并发冲突 | ||
| 184 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(null); | ||
| 185 | + when(imFriendMapper.insert(any(ImFriendDO.class))) | ||
| 186 | + .thenThrow(new DuplicateKeyException("concurrent insert")); | ||
| 187 | + | ||
| 188 | + // 调用:应被吞掉,不抛异常 | ||
| 189 | + friendService.addFriend0(1L, 2L); | ||
| 190 | + | ||
| 191 | + verify(imFriendMapper).insert(any(ImFriendDO.class)); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + // ========== deleteFriend ========== | ||
| 195 | + | ||
| 196 | + @Test | ||
| 197 | + public void testDeleteFriend_success() { | ||
| 198 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 199 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImFriendServiceImpl.class))) | ||
| 200 | + .thenReturn(friendService); | ||
| 201 | + | ||
| 202 | + ImFriendDO friend12 = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | ||
| 203 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 204 | + ImFriendDO friend21 = ImFriendDO.builder().id(11L).userId(2L).friendUserId(1L) | ||
| 205 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 206 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(friend12); | ||
| 207 | + when(imFriendMapper.selectByUserIdAndFriendUserId(2L, 1L)).thenReturn(friend21); | ||
| 208 | + | ||
| 209 | + friendService.deleteFriend(1L, 2L); | ||
| 210 | + | ||
| 211 | + // 断言:双向更新为 DISABLE | ||
| 212 | + verify(imFriendMapper, times(2)).updateById(any(ImFriendDO.class)); | ||
| 213 | + // 断言:推送提示消息 + 双方各推送一次好友删除事件 | ||
| 214 | + verify(privateMessageService).sendTipPrivateMessage(eq(1L), eq(2L), anyString()); | ||
| 215 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | ||
| 216 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); | ||
| 217 | + } | ||
| 218 | + } | ||
| 219 | + | ||
| 220 | + @Test | ||
| 221 | + public void testDeleteFriend0_alreadyDisabled() { | ||
| 222 | + // 准备:已经是 DISABLE,不再更新 | ||
| 223 | + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | ||
| 224 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 225 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); | ||
| 226 | + | ||
| 227 | + friendService.deleteFriend0(1L, 2L); | ||
| 228 | + | ||
| 229 | + verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); | ||
| 230 | + } | ||
| 231 | + | ||
| 232 | + @Test | ||
| 233 | + public void testDeleteFriend0_enabledGetsDisabled() { | ||
| 234 | + ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | ||
| 235 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 236 | + when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); | ||
| 237 | + | ||
| 238 | + friendService.deleteFriend0(1L, 2L); | ||
| 239 | + | ||
| 240 | + ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); | ||
| 241 | + verify(imFriendMapper).updateById(captor.capture()); | ||
| 242 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); | ||
| 243 | + assertNotNull(captor.getValue().getDeleteTime()); | ||
| 244 | + } | ||
| 245 | + | ||
| 246 | + // ========== 其它读方法 ========== | ||
| 247 | + | ||
| 248 | + @Test | ||
| 249 | + public void testGetFriendList() { | ||
| 250 | + List<ImFriendDO> list = List.of( | ||
| 251 | + ImFriendDO.builder().id(1L).userId(1L).friendUserId(2L) | ||
| 252 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 253 | + ImFriendDO.builder().id(2L).userId(1L).friendUserId(3L) | ||
| 254 | + .status(CommonStatusEnum.DISABLE.getStatus()) | ||
| 255 | + .deleteTime(LocalDateTime.now()).build() | ||
| 256 | + ); | ||
| 257 | + when(imFriendMapper.selectListByUserId(1L)).thenReturn(list); | ||
| 258 | + | ||
| 259 | + List<ImFriendDO> result = friendService.getFriendList(1L); | ||
| 260 | + assertEquals(2, result.size()); | ||
| 261 | + } | ||
| 262 | + | ||
| 263 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.group; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 5 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 6 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberUpdateReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; | ||
| 9 | +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | ||
| 10 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | ||
| 11 | +import org.junit.jupiter.api.Test; | ||
| 12 | +import org.mockito.ArgumentCaptor; | ||
| 13 | +import org.mockito.InjectMocks; | ||
| 14 | +import org.mockito.Mock; | ||
| 15 | +import org.springframework.dao.DuplicateKeyException; | ||
| 16 | + | ||
| 17 | +import java.util.List; | ||
| 18 | + | ||
| 19 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.GROUP_MEMBER_NOT_IN_GROUP; | ||
| 20 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 21 | +import static org.mockito.ArgumentMatchers.*; | ||
| 22 | +import static org.mockito.Mockito.*; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * IM 群成员 Service 单元测试 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 30 | + | ||
| 31 | + @InjectMocks | ||
| 32 | + private ImGroupMemberServiceImpl groupMemberService; | ||
| 33 | + | ||
| 34 | + @Mock | ||
| 35 | + private ImGroupMemberMapper groupMemberMapper; | ||
| 36 | + @Mock | ||
| 37 | + private ImWebSocketService webSocketService; | ||
| 38 | + | ||
| 39 | + // ========== addGroupMember ========== | ||
| 40 | + | ||
| 41 | + @Test | ||
| 42 | + public void testAddGroupMember_newInsert() { | ||
| 43 | + // 准备:成员记录不存在 | ||
| 44 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null); | ||
| 45 | + | ||
| 46 | + // 调用 | ||
| 47 | + ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | ||
| 48 | + | ||
| 49 | + // 断言:执行了 insert,返回记录的 status 为 ENABLE | ||
| 50 | + assertNotNull(result); | ||
| 51 | + assertEquals(10L, result.getGroupId()); | ||
| 52 | + assertEquals(1L, result.getUserId()); | ||
| 53 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); | ||
| 54 | + assertNotNull(result.getJoinTime()); | ||
| 55 | + verify(groupMemberMapper).insert(any(ImGroupMemberDO.class)); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + @Test | ||
| 59 | + public void testAddGroupMember_existingEnabledReturns() { | ||
| 60 | + // 准备:已存在且 ENABLE,只返回已有记录,不做其它操作 | ||
| 61 | + ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 62 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 63 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); | ||
| 64 | + when(groupMemberMapper.selectById(50L)).thenReturn(exists); | ||
| 65 | + | ||
| 66 | + ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | ||
| 67 | + | ||
| 68 | + assertEquals(50L, result.getId()); | ||
| 69 | + verify(groupMemberMapper, never()).insert(any(ImGroupMemberDO.class)); | ||
| 70 | + verify(groupMemberMapper, never()).updateById(any(ImGroupMemberDO.class)); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + @Test | ||
| 74 | + public void testAddGroupMember_existingDisabledRecovers() { | ||
| 75 | + // 准备:已存在且 DISABLE,应更新为 ENABLE | ||
| 76 | + ImGroupMemberDO exists = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 77 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 78 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(exists); | ||
| 79 | + ImGroupMemberDO recovered = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 80 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 81 | + when(groupMemberMapper.selectById(50L)).thenReturn(recovered); | ||
| 82 | + | ||
| 83 | + ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | ||
| 84 | + | ||
| 85 | + ArgumentCaptor<ImGroupMemberDO> captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); | ||
| 86 | + verify(groupMemberMapper).updateById(captor.capture()); | ||
| 87 | + assertEquals(50L, captor.getValue().getId()); | ||
| 88 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | ||
| 89 | + assertNotNull(captor.getValue().getJoinTime()); | ||
| 90 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); | ||
| 91 | + } | ||
| 92 | + | ||
| 93 | + @Test | ||
| 94 | + public void testAddGroupMember_duplicateKeyFallsBackToSelect() { | ||
| 95 | + // 准备:第一次 select 返回 null,insert 抛 DuplicateKey,再次 select 返回已插入记录 | ||
| 96 | + ImGroupMemberDO inserted = ImGroupMemberDO.builder().id(80L).groupId(10L).userId(1L) | ||
| 97 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 98 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null).thenReturn(inserted); | ||
| 99 | + when(groupMemberMapper.insert(any(ImGroupMemberDO.class))) | ||
| 100 | + .thenThrow(new DuplicateKeyException("concurrent insert")); | ||
| 101 | + | ||
| 102 | + // 调用:冲突后降级 select | ||
| 103 | + ImGroupMemberDO result = groupMemberService.addGroupMember(10L, 1L); | ||
| 104 | + | ||
| 105 | + assertNotNull(result); | ||
| 106 | + assertEquals(80L, result.getId()); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + // ========== addGroupMembers ========== | ||
| 110 | + | ||
| 111 | + @Test | ||
| 112 | + public void testAddGroupMembers_mixedInsertAndUpdate() { | ||
| 113 | + // 准备:用户 2 不存在(新增),用户 3 已存在且 DISABLE(恢复),用户 4 已存在且 ENABLE(跳过) | ||
| 114 | + ImGroupMemberDO exist3 = ImGroupMemberDO.builder().id(30L).groupId(10L).userId(3L) | ||
| 115 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 116 | + ImGroupMemberDO exist4 = ImGroupMemberDO.builder().id(40L).groupId(10L).userId(4L) | ||
| 117 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 118 | + when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) | ||
| 119 | + .thenReturn(List.of(exist3, exist4)); | ||
| 120 | + | ||
| 121 | + // 调用 | ||
| 122 | + groupMemberService.addGroupMembers(10L, List.of(2L, 3L, 4L)); | ||
| 123 | + | ||
| 124 | + // 断言:updates 只有用户 3;inserts 只有用户 2 | ||
| 125 | + verify(groupMemberMapper).updateBatch(argThat((List<ImGroupMemberDO> list) -> | ||
| 126 | + list.size() == 1 && list.get(0).getId().equals(30L) | ||
| 127 | + && CommonStatusEnum.ENABLE.getStatus().equals(list.get(0).getStatus()))); | ||
| 128 | + verify(groupMemberMapper).insertBatch(argThat((List<ImGroupMemberDO> list) -> | ||
| 129 | + list.size() == 1 && list.get(0).getUserId().equals(2L))); | ||
| 130 | + } | ||
| 131 | + | ||
| 132 | + @Test | ||
| 133 | + public void testAddGroupMembers_batchInsertDuplicateFallback() { | ||
| 134 | + // 准备:两个新增成员,批量插入失败时降级为逐个 addGroupMember | ||
| 135 | + when(groupMemberMapper.selectListByGroupIdAndUserIds(eq(10L), anyCollection())) | ||
| 136 | + .thenReturn(List.of()); | ||
| 137 | + doThrow(new DuplicateKeyException("concurrent batch insert")) | ||
| 138 | + .when(groupMemberMapper).insertBatch(anyList()); | ||
| 139 | + // addGroupMember 单条兜底逻辑 | ||
| 140 | + when(groupMemberMapper.selectByGroupIdAndUserId(eq(10L), anyLong())).thenReturn(null); | ||
| 141 | + | ||
| 142 | + // 调用 | ||
| 143 | + groupMemberService.addGroupMembers(10L, List.of(2L, 3L)); | ||
| 144 | + | ||
| 145 | + // 断言:降级为逐条调用 insert | ||
| 146 | + verify(groupMemberMapper, times(2)).insert(any(ImGroupMemberDO.class)); | ||
| 147 | + } | ||
| 148 | + | ||
| 149 | + // ========== validateMemberInGroup ========== | ||
| 150 | + | ||
| 151 | + @Test | ||
| 152 | + public void testValidateMemberInGroup_notInGroup() { | ||
| 153 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null); | ||
| 154 | + | ||
| 155 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 156 | + () -> groupMemberService.validateMemberInGroup(10L, 1L)); | ||
| 157 | + assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 158 | + } | ||
| 159 | + | ||
| 160 | + @Test | ||
| 161 | + public void testValidateMemberInGroup_disabledEqualsNotInGroup() { | ||
| 162 | + ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 163 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 164 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); | ||
| 165 | + | ||
| 166 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 167 | + () -> groupMemberService.validateMemberInGroup(10L, 1L)); | ||
| 168 | + assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 169 | + } | ||
| 170 | + | ||
| 171 | + @Test | ||
| 172 | + public void testValidateMemberInGroup_success() { | ||
| 173 | + ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 174 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 175 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); | ||
| 176 | + | ||
| 177 | + ImGroupMemberDO result = groupMemberService.validateMemberInGroup(10L, 1L); | ||
| 178 | + assertEquals(50L, result.getId()); | ||
| 179 | + } | ||
| 180 | + | ||
| 181 | + // ========== updateGroupMember ========== | ||
| 182 | + | ||
| 183 | + @Test | ||
| 184 | + public void testUpdateGroupMember_success() { | ||
| 185 | + ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 186 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 187 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); | ||
| 188 | + | ||
| 189 | + ImGroupMemberUpdateReqVO reqVO = new ImGroupMemberUpdateReqVO() | ||
| 190 | + .setGroupId(10L).setMuted(true).setDisplayUserName("昵称"); | ||
| 191 | + | ||
| 192 | + groupMemberService.updateGroupMember(1L, reqVO); | ||
| 193 | + | ||
| 194 | + ArgumentCaptor<ImGroupMemberDO> captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); | ||
| 195 | + verify(groupMemberMapper).updateById(captor.capture()); | ||
| 196 | + assertEquals(50L, captor.getValue().getId()); | ||
| 197 | + assertTrue(captor.getValue().getMuted()); | ||
| 198 | + verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); | ||
| 199 | + } | ||
| 200 | + | ||
| 201 | + // ========== removeGroupMember ========== | ||
| 202 | + | ||
| 203 | + @Test | ||
| 204 | + public void testRemoveGroupMember_success() { | ||
| 205 | + ImGroupMemberDO member = ImGroupMemberDO.builder().id(50L).groupId(10L).userId(1L) | ||
| 206 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 207 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(member); | ||
| 208 | + | ||
| 209 | + groupMemberService.removeGroupMember(10L, 1L); | ||
| 210 | + | ||
| 211 | + ArgumentCaptor<ImGroupMemberDO> captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); | ||
| 212 | + verify(groupMemberMapper).updateById(captor.capture()); | ||
| 213 | + assertEquals(50L, captor.getValue().getId()); | ||
| 214 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); | ||
| 215 | + assertNotNull(captor.getValue().getQuitTime()); | ||
| 216 | + } | ||
| 217 | + | ||
| 218 | + @Test | ||
| 219 | + public void testRemoveGroupMember_notInGroup() { | ||
| 220 | + when(groupMemberMapper.selectByGroupIdAndUserId(10L, 1L)).thenReturn(null); | ||
| 221 | + | ||
| 222 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 223 | + () -> groupMemberService.removeGroupMember(10L, 1L)); | ||
| 224 | + assertEquals(GROUP_MEMBER_NOT_IN_GROUP.getCode(), exception.getCode()); | ||
| 225 | + } | ||
| 226 | + | ||
| 227 | + // ========== removeGroupMembers ========== | ||
| 228 | + | ||
| 229 | + @Test | ||
| 230 | + public void testRemoveGroupMembers_batch() { | ||
| 231 | + groupMemberService.removeGroupMembers(10L, List.of(2L, 3L)); | ||
| 232 | + | ||
| 233 | + ArgumentCaptor<ImGroupMemberDO> captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); | ||
| 234 | + verify(groupMemberMapper).updateByGroupIdAndUserIdsAndStatus(eq(10L), anyCollection(), | ||
| 235 | + eq(CommonStatusEnum.ENABLE.getStatus()), captor.capture()); | ||
| 236 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); | ||
| 237 | + assertNotNull(captor.getValue().getQuitTime()); | ||
| 238 | + } | ||
| 239 | + | ||
| 240 | + @Test | ||
| 241 | + public void testRemoveGroupMembersByGroupId() { | ||
| 242 | + groupMemberService.removeGroupMembersByGroupId(10L); | ||
| 243 | + | ||
| 244 | + ArgumentCaptor<ImGroupMemberDO> captor = ArgumentCaptor.forClass(ImGroupMemberDO.class); | ||
| 245 | + verify(groupMemberMapper).updateByGroupIdAndStatus(eq(10L), | ||
| 246 | + eq(CommonStatusEnum.ENABLE.getStatus()), captor.capture()); | ||
| 247 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); | ||
| 248 | + assertNotNull(captor.getValue().getQuitTime()); | ||
| 249 | + } | ||
| 250 | + | ||
| 251 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.group; | ||
| 2 | + | ||
| 3 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 5 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 6 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 7 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupCreateReqVO; | ||
| 8 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.ImGroupUpdateReqVO; | ||
| 9 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberInviteReqVO; | ||
| 10 | +import cn.iocoder.yudao.module.im.controller.admin.group.vo.member.ImGroupMemberRemoveReqVO; | ||
| 11 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 12 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | ||
| 13 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 14 | +import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; | ||
| 15 | +import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | ||
| 16 | +import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; | ||
| 17 | +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | ||
| 18 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | ||
| 19 | +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; | ||
| 20 | +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; | ||
| 21 | +import org.junit.jupiter.api.Test; | ||
| 22 | +import org.mockito.ArgumentCaptor; | ||
| 23 | +import org.mockito.InjectMocks; | ||
| 24 | +import org.mockito.Mock; | ||
| 25 | +import org.mockito.MockedStatic; | ||
| 26 | + | ||
| 27 | +import java.time.LocalDateTime; | ||
| 28 | +import java.util.ArrayList; | ||
| 29 | +import java.util.HashMap; | ||
| 30 | +import java.util.List; | ||
| 31 | +import java.util.Map; | ||
| 32 | + | ||
| 33 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | ||
| 34 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 35 | +import static org.mockito.ArgumentMatchers.*; | ||
| 36 | +import static org.mockito.Mockito.*; | ||
| 37 | + | ||
| 38 | +/** | ||
| 39 | + * IM 群 Service 单元测试 | ||
| 40 | + * | ||
| 41 | + * @author 芋道源码 | ||
| 42 | + */ | ||
| 43 | +public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | ||
| 44 | + | ||
| 45 | + @InjectMocks | ||
| 46 | + private ImGroupServiceImpl groupService; | ||
| 47 | + | ||
| 48 | + @Mock | ||
| 49 | + private ImGroupMapper groupMapper; | ||
| 50 | + @Mock | ||
| 51 | + private ImGroupMemberService groupMemberService; | ||
| 52 | + @Mock | ||
| 53 | + private ImGroupMessageService groupMessageService; | ||
| 54 | + @Mock | ||
| 55 | + private ImWebSocketService webSocketService; | ||
| 56 | + @Mock | ||
| 57 | + private ImFriendService friendService; | ||
| 58 | + @Mock | ||
| 59 | + private AdminUserApi adminUserApi; | ||
| 60 | + | ||
| 61 | + // ========== createGroup ========== | ||
| 62 | + | ||
| 63 | + @Test | ||
| 64 | + public void testCreateGroup_success() { | ||
| 65 | + // 准备 | ||
| 66 | + ImGroupCreateReqVO reqVO = new ImGroupCreateReqVO(); | ||
| 67 | + reqVO.setName("测试群"); | ||
| 68 | + when(groupMapper.insert(any(ImGroupDO.class))).thenAnswer(invocation -> { | ||
| 69 | + ImGroupDO group = invocation.getArgument(0); | ||
| 70 | + group.setId(100L); | ||
| 71 | + return 1; | ||
| 72 | + }); | ||
| 73 | + | ||
| 74 | + // 调用 | ||
| 75 | + ImGroupDO result = groupService.createGroup(reqVO, 1L); | ||
| 76 | + | ||
| 77 | + // 断言:群主 + 状态 | ||
| 78 | + assertEquals(100L, result.getId()); | ||
| 79 | + assertEquals(1L, result.getOwnerUserId()); | ||
| 80 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), result.getStatus()); | ||
| 81 | + // 验证:群主加入群 + 推送群创建事件 | ||
| 82 | + verify(groupMemberService).addGroupMember(100L, 1L); | ||
| 83 | + verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); | ||
| 84 | + } | ||
| 85 | + | ||
| 86 | + // ========== updateGroup ========== | ||
| 87 | + | ||
| 88 | + @Test | ||
| 89 | + public void testUpdateGroup_notOwner() { | ||
| 90 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 91 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 92 | + .thenReturn(groupService); | ||
| 93 | + | ||
| 94 | + // 准备:当前用户不是群主 | ||
| 95 | + ImGroupDO group = ImGroupDO.builder().id(10L).name("群").ownerUserId(99L) | ||
| 96 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 97 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 98 | + | ||
| 99 | + ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO(); | ||
| 100 | + reqVO.setId(10L); | ||
| 101 | + reqVO.setName("新名字"); | ||
| 102 | + | ||
| 103 | + // 调用并断言 | ||
| 104 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 105 | + () -> groupService.updateGroup(reqVO, 1L)); | ||
| 106 | + assertEquals(GROUP_NOT_OWNER.getCode(), exception.getCode()); | ||
| 107 | + } | ||
| 108 | + } | ||
| 109 | + | ||
| 110 | + @Test | ||
| 111 | + public void testUpdateGroup_success() { | ||
| 112 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 113 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 114 | + .thenReturn(groupService); | ||
| 115 | + | ||
| 116 | + // 准备 | ||
| 117 | + ImGroupDO group = ImGroupDO.builder().id(10L).name("旧名字").ownerUserId(1L) | ||
| 118 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 119 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 120 | + List<ImGroupMemberDO> members = List.of( | ||
| 121 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 122 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 123 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | ||
| 124 | + .status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 125 | + ); | ||
| 126 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members); | ||
| 127 | + | ||
| 128 | + ImGroupUpdateReqVO reqVO = new ImGroupUpdateReqVO(); | ||
| 129 | + reqVO.setId(10L); | ||
| 130 | + reqVO.setName("新名字"); | ||
| 131 | + | ||
| 132 | + // 调用 | ||
| 133 | + ImGroupDO result = groupService.updateGroup(reqVO, 1L); | ||
| 134 | + | ||
| 135 | + // 断言:更新了数据库 | ||
| 136 | + verify(groupMapper).updateById(any(ImGroupDO.class)); | ||
| 137 | + assertEquals("新名字", result.getName()); | ||
| 138 | + // 推送给所有成员 | ||
| 139 | + verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); | ||
| 140 | + } | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + // ========== dissolveGroup ========== | ||
| 144 | + | ||
| 145 | + @Test | ||
| 146 | + public void testDissolveGroup_success() { | ||
| 147 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 148 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 149 | + .thenReturn(groupService); | ||
| 150 | + | ||
| 151 | + // 准备 | ||
| 152 | + ImGroupDO group = ImGroupDO.builder().id(10L).name("群").ownerUserId(1L) | ||
| 153 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 154 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 155 | + List<ImGroupMemberDO> members = List.of( | ||
| 156 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 157 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 158 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | ||
| 159 | + .status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 160 | + ); | ||
| 161 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(members); | ||
| 162 | + AdminUserRespDTO user = new AdminUserRespDTO(); | ||
| 163 | + user.setId(1L); | ||
| 164 | + user.setNickname("群主"); | ||
| 165 | + when(adminUserApi.getUser(1L)).thenReturn(user); | ||
| 166 | + | ||
| 167 | + // 调用 | ||
| 168 | + groupService.dissolveGroup(10L, 1L); | ||
| 169 | + | ||
| 170 | + // 断言:群状态变为 DISABLE + 群成员全部移除 + 清理已读缓存 | ||
| 171 | + ArgumentCaptor<ImGroupDO> captor = ArgumentCaptor.forClass(ImGroupDO.class); | ||
| 172 | + verify(groupMapper).updateById(captor.capture()); | ||
| 173 | + assertEquals(CommonStatusEnum.DISABLE.getStatus(), captor.getValue().getStatus()); | ||
| 174 | + assertNotNull(captor.getValue().getDissolvedTime()); | ||
| 175 | + verify(groupMemberService).removeGroupMembersByGroupId(10L); | ||
| 176 | + verify(groupMessageService).deleteReadMaxMessageIdMap(10L); | ||
| 177 | + // 发送解散提示消息 + 推送群删除事件 | ||
| 178 | + verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); | ||
| 179 | + verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); | ||
| 180 | + } | ||
| 181 | + } | ||
| 182 | + | ||
| 183 | + @Test | ||
| 184 | + public void testDissolveGroup_notOwner() { | ||
| 185 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 186 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 187 | + .thenReturn(groupService); | ||
| 188 | + | ||
| 189 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) | ||
| 190 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 191 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 192 | + | ||
| 193 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 194 | + () -> groupService.dissolveGroup(10L, 1L)); | ||
| 195 | + assertEquals(GROUP_NOT_OWNER.getCode(), exception.getCode()); | ||
| 196 | + } | ||
| 197 | + } | ||
| 198 | + | ||
| 199 | + // ========== inviteGroupMember ========== | ||
| 200 | + | ||
| 201 | + @Test | ||
| 202 | + public void testInviteGroupMember_success() { | ||
| 203 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 204 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 205 | + .thenReturn(groupService); | ||
| 206 | + | ||
| 207 | + // 准备:群存在 + 当前用户是群成员 | ||
| 208 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | ||
| 209 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 210 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 211 | + | ||
| 212 | + // 当前成员只有群主 | ||
| 213 | + List<ImGroupMemberDO> activeMembers = new ArrayList<>(); | ||
| 214 | + activeMembers.add(ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 215 | + .status(CommonStatusEnum.ENABLE.getStatus()).build()); | ||
| 216 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(activeMembers); | ||
| 217 | + | ||
| 218 | + // 被邀请人 2 和 3 都是好友 | ||
| 219 | + ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO(); | ||
| 220 | + reqVO.setGroupId(10L); | ||
| 221 | + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L))); | ||
| 222 | + List<ImFriendDO> friends = List.of( | ||
| 223 | + ImFriendDO.builder().userId(1L).friendUserId(2L) | ||
| 224 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 225 | + ImFriendDO.builder().userId(1L).friendUserId(3L) | ||
| 226 | + .status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 227 | + ); | ||
| 228 | + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(friends); | ||
| 229 | + | ||
| 230 | + // 用户昵称 | ||
| 231 | + AdminUserRespDTO u1 = new AdminUserRespDTO(); u1.setId(1L); u1.setNickname("群主"); | ||
| 232 | + AdminUserRespDTO u2 = new AdminUserRespDTO(); u2.setId(2L); u2.setNickname("张三"); | ||
| 233 | + AdminUserRespDTO u3 = new AdminUserRespDTO(); u3.setId(3L); u3.setNickname("李四"); | ||
| 234 | + Map<Long, AdminUserRespDTO> userMap23 = new HashMap<>(); | ||
| 235 | + userMap23.put(2L, u2); | ||
| 236 | + userMap23.put(3L, u3); | ||
| 237 | + Map<Long, AdminUserRespDTO> userMap1 = Map.of(1L, u1); | ||
| 238 | + when(adminUserApi.getUserMap(List.of(2L, 3L))).thenReturn(userMap23); | ||
| 239 | + when(adminUserApi.getUserMap(List.of(1L))).thenReturn(userMap1); | ||
| 240 | + | ||
| 241 | + // 调用 | ||
| 242 | + groupService.inviteGroupMember(1L, reqVO); | ||
| 243 | + | ||
| 244 | + // 断言:校验群成员 + 批量添加成员 + 推送事件 + 发送提示消息 | ||
| 245 | + verify(groupMemberService).validateMemberInGroup(10L, 1L); | ||
| 246 | + verify(groupMemberService).addGroupMembers(eq(10L), anyCollection()); | ||
| 247 | + verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); | ||
| 248 | + verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); | ||
| 249 | + } | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + @Test | ||
| 253 | + public void testInviteGroupMember_notFriend() { | ||
| 254 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 255 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 256 | + .thenReturn(groupService); | ||
| 257 | + | ||
| 258 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | ||
| 259 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 260 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 261 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)) | ||
| 262 | + .thenReturn(List.of(ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 263 | + .status(CommonStatusEnum.ENABLE.getStatus()).build())); | ||
| 264 | + | ||
| 265 | + ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO(); | ||
| 266 | + reqVO.setGroupId(10L); | ||
| 267 | + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L, 3L))); | ||
| 268 | + // 只有 2 是好友,3 不是 | ||
| 269 | + when(friendService.getActiveFriendList(eq(1L), anyCollection())).thenReturn(List.of( | ||
| 270 | + ImFriendDO.builder().userId(1L).friendUserId(2L) | ||
| 271 | + .status(CommonStatusEnum.ENABLE.getStatus()).build())); | ||
| 272 | + AdminUserRespDTO u3 = new AdminUserRespDTO(); u3.setId(3L); u3.setNickname("李四"); | ||
| 273 | + when(adminUserApi.getUserMap(anyCollection())).thenReturn(Map.of(3L, u3)); | ||
| 274 | + | ||
| 275 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 276 | + () -> groupService.inviteGroupMember(1L, reqVO)); | ||
| 277 | + assertEquals(GROUP_INVITE_NOT_FRIEND.getCode(), exception.getCode()); | ||
| 278 | + } | ||
| 279 | + } | ||
| 280 | + | ||
| 281 | + @Test | ||
| 282 | + public void testInviteGroupMember_skipsMembersAlreadyInGroup() { | ||
| 283 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 284 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 285 | + .thenReturn(groupService); | ||
| 286 | + | ||
| 287 | + // 准备 | ||
| 288 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | ||
| 289 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 290 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 291 | + // 用户 2 已在群中 | ||
| 292 | + when(groupMemberService.getActiveGroupMemberListByGroupId(10L)).thenReturn(List.of( | ||
| 293 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 294 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 295 | + ImGroupMemberDO.builder().groupId(10L).userId(2L) | ||
| 296 | + .status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 297 | + )); | ||
| 298 | + | ||
| 299 | + ImGroupMemberInviteReqVO reqVO = new ImGroupMemberInviteReqVO(); | ||
| 300 | + reqVO.setGroupId(10L); | ||
| 301 | + reqVO.setMemberUserIds(new ArrayList<>(List.of(2L))); // 只邀请 2,他已在群中 | ||
| 302 | + | ||
| 303 | + // 调用 | ||
| 304 | + groupService.inviteGroupMember(1L, reqVO); | ||
| 305 | + | ||
| 306 | + // 断言:不会触发添加、不会推送 | ||
| 307 | + verify(groupMemberService, never()).addGroupMembers(anyLong(), anyCollection()); | ||
| 308 | + verify(webSocketService, never()).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); | ||
| 309 | + } | ||
| 310 | + } | ||
| 311 | + | ||
| 312 | + // ========== quitGroup ========== | ||
| 313 | + | ||
| 314 | + @Test | ||
| 315 | + public void testQuitGroup_ownerCannotQuit() { | ||
| 316 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 317 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 318 | + .thenReturn(groupService); | ||
| 319 | + | ||
| 320 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | ||
| 321 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 322 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 323 | + | ||
| 324 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 325 | + () -> groupService.quitGroup(10L, 1L)); | ||
| 326 | + assertEquals(GROUP_OWNER_CANNOT_QUIT.getCode(), exception.getCode()); | ||
| 327 | + } | ||
| 328 | + } | ||
| 329 | + | ||
| 330 | + @Test | ||
| 331 | + public void testQuitGroup_success() { | ||
| 332 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 333 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 334 | + .thenReturn(groupService); | ||
| 335 | + | ||
| 336 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(99L) | ||
| 337 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 338 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 339 | + | ||
| 340 | + groupService.quitGroup(10L, 1L); | ||
| 341 | + | ||
| 342 | + verify(groupMemberService).removeGroupMember(10L, 1L); | ||
| 343 | + verify(groupMessageService).deleteReadMaxMessageId(10L, 1L); | ||
| 344 | + verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); | ||
| 345 | + verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); | ||
| 346 | + } | ||
| 347 | + } | ||
| 348 | + | ||
| 349 | + // ========== removeGroupMember ========== | ||
| 350 | + | ||
| 351 | + @Test | ||
| 352 | + public void testRemoveGroupMember_cannotRemoveSelf() { | ||
| 353 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 354 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 355 | + .thenReturn(groupService); | ||
| 356 | + | ||
| 357 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | ||
| 358 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 359 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 360 | + | ||
| 361 | + ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); | ||
| 362 | + reqVO.setGroupId(10L); | ||
| 363 | + reqVO.setMemberUserIds(List.of(1L, 2L)); | ||
| 364 | + | ||
| 365 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 366 | + () -> groupService.removeGroupMember(1L, reqVO)); | ||
| 367 | + assertEquals(GROUP_CANNOT_REMOVE_SELF.getCode(), exception.getCode()); | ||
| 368 | + } | ||
| 369 | + } | ||
| 370 | + | ||
| 371 | + @Test | ||
| 372 | + public void testRemoveGroupMember_success() { | ||
| 373 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 374 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 375 | + .thenReturn(groupService); | ||
| 376 | + | ||
| 377 | + ImGroupDO group = ImGroupDO.builder().id(10L).ownerUserId(1L) | ||
| 378 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 379 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 380 | + | ||
| 381 | + ImGroupMemberRemoveReqVO reqVO = new ImGroupMemberRemoveReqVO(); | ||
| 382 | + reqVO.setGroupId(10L); | ||
| 383 | + reqVO.setMemberUserIds(List.of(2L, 3L)); | ||
| 384 | + | ||
| 385 | + groupService.removeGroupMember(1L, reqVO); | ||
| 386 | + | ||
| 387 | + verify(groupMemberService).removeGroupMembers(eq(10L), anyCollection()); | ||
| 388 | + verify(groupMessageService).deleteReadMaxMessageIds(eq(10L), anyCollection()); | ||
| 389 | + verify(groupMessageService).sendTipGroupMessage(eq(1L), eq(10L), anySet(), anyString()); | ||
| 390 | + verify(webSocketService).sendGroupMessageAsync(anyCollection(), any(ImGroupMessageDTO.class)); | ||
| 391 | + } | ||
| 392 | + } | ||
| 393 | + | ||
| 394 | + // ========== validateGroupExists ========== | ||
| 395 | + | ||
| 396 | + @Test | ||
| 397 | + public void testValidateGroupExists_notExists() { | ||
| 398 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 399 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 400 | + .thenReturn(groupService); | ||
| 401 | + | ||
| 402 | + when(groupMapper.selectById(10L)).thenReturn(null); | ||
| 403 | + | ||
| 404 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 405 | + () -> groupService.validateGroupExists(10L)); | ||
| 406 | + assertEquals(GROUP_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 407 | + } | ||
| 408 | + } | ||
| 409 | + | ||
| 410 | + @Test | ||
| 411 | + public void testValidateGroupExists_banned() { | ||
| 412 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 413 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 414 | + .thenReturn(groupService); | ||
| 415 | + | ||
| 416 | + ImGroupDO group = ImGroupDO.builder().id(10L).banned(true) | ||
| 417 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 418 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 419 | + | ||
| 420 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 421 | + () -> groupService.validateGroupExists(10L)); | ||
| 422 | + assertEquals(GROUP_BANNED.getCode(), exception.getCode()); | ||
| 423 | + } | ||
| 424 | + } | ||
| 425 | + | ||
| 426 | + @Test | ||
| 427 | + public void testValidateGroupExists_dissolved() { | ||
| 428 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 429 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImGroupServiceImpl.class))) | ||
| 430 | + .thenReturn(groupService); | ||
| 431 | + | ||
| 432 | + ImGroupDO group = ImGroupDO.builder().id(10L) | ||
| 433 | + .status(CommonStatusEnum.DISABLE.getStatus()).build(); | ||
| 434 | + when(groupMapper.selectById(10L)).thenReturn(group); | ||
| 435 | + | ||
| 436 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 437 | + () -> groupService.validateGroupExists(10L)); | ||
| 438 | + assertEquals(GROUP_DISSOLVED.getCode(), exception.getCode()); | ||
| 439 | + } | ||
| 440 | + } | ||
| 441 | + | ||
| 442 | + // ========== getActiveGroupList ========== | ||
| 443 | + | ||
| 444 | + @Test | ||
| 445 | + public void testGetActiveGroupList_noMembers() { | ||
| 446 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of()); | ||
| 447 | + | ||
| 448 | + List<ImGroupDO> result = groupService.getActiveGroupList(1L); | ||
| 449 | + assertTrue(result.isEmpty()); | ||
| 450 | + verify(groupMapper, never()).selectListByIds(anyCollection(), any(), any()); | ||
| 451 | + } | ||
| 452 | + | ||
| 453 | + @Test | ||
| 454 | + public void testGetActiveGroupList_success() { | ||
| 455 | + when(groupMemberService.getActiveGroupMemberListByUserId(1L)).thenReturn(List.of( | ||
| 456 | + ImGroupMemberDO.builder().groupId(10L).userId(1L) | ||
| 457 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 458 | + ImGroupMemberDO.builder().groupId(20L).userId(1L) | ||
| 459 | + .status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 460 | + )); | ||
| 461 | + List<ImGroupDO> groups = List.of( | ||
| 462 | + ImGroupDO.builder().id(10L).status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 463 | + ImGroupDO.builder().id(20L).status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 464 | + ); | ||
| 465 | + when(groupMapper.selectListByIds(anyCollection(), eq(CommonStatusEnum.ENABLE.getStatus()), eq(false))) | ||
| 466 | + .thenReturn(groups); | ||
| 467 | + | ||
| 468 | + List<ImGroupDO> result = groupService.getActiveGroupList(1L); | ||
| 469 | + assertEquals(2, result.size()); | ||
| 470 | + } | ||
| 471 | + | ||
| 472 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
| @@ -4,7 +4,6 @@ import cn.hutool.extra.spring.SpringUtil; | @@ -4,7 +4,6 @@ import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; | 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 7 | -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 8 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; | 7 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; |
| 9 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; | 8 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; |
| 10 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | 9 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; |
| @@ -16,12 +15,15 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | @@ -16,12 +15,15 @@ import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 16 | import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; | 15 | import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; |
| 17 | import cn.iocoder.yudao.module.im.service.group.ImGroupService; | 16 | import cn.iocoder.yudao.module.im.service.group.ImGroupService; |
| 18 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | 17 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; |
| 18 | +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | ||
| 19 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | ||
| 19 | import org.junit.jupiter.api.Test; | 20 | import org.junit.jupiter.api.Test; |
| 20 | import org.mockito.InjectMocks; | 21 | import org.mockito.InjectMocks; |
| 21 | import org.mockito.Mock; | 22 | import org.mockito.Mock; |
| 22 | import org.mockito.MockedStatic; | 23 | import org.mockito.MockedStatic; |
| 23 | 24 | ||
| 24 | import java.time.LocalDateTime; | 25 | import java.time.LocalDateTime; |
| 26 | +import java.util.Collection; | ||
| 25 | import java.util.HashMap; | 27 | import java.util.HashMap; |
| 26 | import java.util.List; | 28 | import java.util.List; |
| 27 | import java.util.Map; | 29 | import java.util.Map; |
| @@ -52,7 +54,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -52,7 +54,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 52 | @Mock | 54 | @Mock |
| 53 | private GroupMessageReadRedisDAO groupMessageReadRedisDAO; | 55 | private GroupMessageReadRedisDAO groupMessageReadRedisDAO; |
| 54 | @Mock | 56 | @Mock |
| 55 | - private WebSocketMessageSender webSocketMessageSender; | 57 | + private ImWebSocketService imWebSocketService; |
| 56 | 58 | ||
| 57 | private ImGroupMessageSendReqVO buildSendReqVO() { | 59 | private ImGroupMessageSendReqVO buildSendReqVO() { |
| 58 | ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO(); | 60 | ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO(); |
| @@ -109,7 +111,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -109,7 +111,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 109 | assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus()); | 111 | assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus()); |
| 110 | 112 | ||
| 111 | // 验证推送给 3 个群成员(含发送者自己,用于多端同步) | 113 | // 验证推送给 3 个群成员(含发送者自己,用于多端同步) |
| 112 | - verify(webSocketMessageSender, times(3)).sendObject(anyInt(), anyLong(), anyString(), any()); | 114 | + verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> |
| 115 | + ids.size() == 3 && ids.contains(1L) && ids.contains(2L) && ids.contains(3L)), | ||
| 116 | + any(ImGroupMessageDTO.class)); | ||
| 113 | } | 117 | } |
| 114 | } | 118 | } |
| 115 | 119 | ||
| @@ -571,8 +575,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -571,8 +575,10 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 571 | // 验证:更新原消息状态 + 插入 tipMessage | 575 | // 验证:更新原消息状态 + 插入 tipMessage |
| 572 | verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class)); | 576 | verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class)); |
| 573 | verify(groupMessageMapper).insert(any(ImGroupMessageDO.class)); | 577 | verify(groupMessageMapper).insert(any(ImGroupMessageDO.class)); |
| 574 | - // 验证:只给 2 个活跃成员推送撤回提示(userId=3 已退群) | ||
| 575 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | 578 | + // 验证:给 2 个活跃成员推送撤回提示 |
| 579 | + verify(imWebSocketService).sendGroupMessageAsync(argThat((Collection<Long> ids) -> | ||
| 580 | + ids.size() == 2 && ids.contains(1L) && ids.contains(2L)), | ||
| 581 | + any(ImGroupMessageDTO.class)); | ||
| 576 | } | 582 | } |
| 577 | } | 583 | } |
| 578 | 584 |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
| 1 | package cn.iocoder.yudao.module.im.service.message; | 1 | package cn.iocoder.yudao.module.im.service.message; |
| 2 | 2 | ||
| 3 | -import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | import cn.iocoder.yudao.framework.common.exception.ServiceException; | 3 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 5 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | 4 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 6 | -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 7 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; | 5 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; |
| 8 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | 6 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; |
| 9 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; | 7 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; |
| 10 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | 8 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 9 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | ||
| 11 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | 10 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 12 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | 11 | import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; |
| 12 | +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | ||
| 13 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | ||
| 13 | import org.junit.jupiter.api.Test; | 14 | import org.junit.jupiter.api.Test; |
| 15 | +import org.mockito.ArgumentCaptor; | ||
| 14 | import org.mockito.InjectMocks; | 16 | import org.mockito.InjectMocks; |
| 15 | import org.mockito.Mock; | 17 | import org.mockito.Mock; |
| 16 | -import org.mockito.MockedStatic; | ||
| 17 | 18 | ||
| 18 | import java.time.LocalDateTime; | 19 | import java.time.LocalDateTime; |
| 19 | import java.util.List; | 20 | import java.util.List; |
| @@ -34,13 +35,13 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -34,13 +35,13 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 34 | private ImPrivateMessageServiceImpl privateMessageService; | 35 | private ImPrivateMessageServiceImpl privateMessageService; |
| 35 | 36 | ||
| 36 | @Mock | 37 | @Mock |
| 37 | - private ImPrivateMessageMapper imPrivateMessageMapper; | 38 | + private ImPrivateMessageMapper privateMessageMapper; |
| 38 | @Mock | 39 | @Mock |
| 39 | - private ImFriendService imFriendService; | 40 | + private ImFriendService friendService; |
| 40 | @Mock | 41 | @Mock |
| 41 | - private ImSensitiveWordService imSensitiveWordService; | 42 | + private ImSensitiveWordService sensitiveWordService; |
| 42 | @Mock | 43 | @Mock |
| 43 | - private WebSocketMessageSender webSocketMessageSender; | 44 | + private ImWebSocketService imWebSocketService; |
| 44 | 45 | ||
| 45 | private ImPrivateMessageSendReqVO buildSendReqVO() { | 46 | private ImPrivateMessageSendReqVO buildSendReqVO() { |
| 46 | ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); | 47 | ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); |
| @@ -55,37 +56,35 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -55,37 +56,35 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 55 | 56 | ||
| 56 | @Test | 57 | @Test |
| 57 | public void testSendMessage_success() { | 58 | public void testSendMessage_success() { |
| 58 | - try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 59 | - springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) | ||
| 60 | - .thenReturn(privateMessageService); | ||
| 61 | - | ||
| 62 | - // 准备 | ||
| 63 | - ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 64 | - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | ||
| 65 | - .thenReturn(null); | ||
| 66 | - when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> { | ||
| 67 | - ImPrivateMessageDO msg = invocation.getArgument(0); | ||
| 68 | - msg.setId(99L); | ||
| 69 | - return 1; | ||
| 70 | - }); | ||
| 71 | - | ||
| 72 | - // 调用 | ||
| 73 | - ImPrivateMessageDO result = privateMessageService.sendPrivateMessage(1L, reqVO); | ||
| 74 | - | ||
| 75 | - // 断言 | ||
| 76 | - assertNotNull(result); | ||
| 77 | - assertEquals(1L, result.getSenderId()); | ||
| 78 | - assertEquals(2L, result.getReceiverId()); | ||
| 79 | - assertEquals(0, result.getType()); | ||
| 80 | - assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus()); | ||
| 81 | - assertNotNull(result.getSendTime()); | ||
| 82 | - | ||
| 83 | - // 验证调用 | ||
| 84 | - verify(imFriendService).isFriend(1L, 2L); | ||
| 85 | - verify(imSensitiveWordService).validateText(reqVO.getContent()); | ||
| 86 | - verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class)); | ||
| 87 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 88 | - } | 59 | + // 准备 |
| 60 | + ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); | ||
| 61 | + when(privateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | ||
| 62 | + .thenReturn(null); | ||
| 63 | + when(friendService.isFriend(1L, 2L)).thenReturn(true); | ||
| 64 | + when(privateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> { | ||
| 65 | + ImPrivateMessageDO msg = invocation.getArgument(0); | ||
| 66 | + msg.setId(99L); | ||
| 67 | + return 1; | ||
| 68 | + }); | ||
| 69 | + | ||
| 70 | + // 调用 | ||
| 71 | + ImPrivateMessageDO result = privateMessageService.sendPrivateMessage(1L, reqVO); | ||
| 72 | + | ||
| 73 | + // 断言 | ||
| 74 | + assertNotNull(result); | ||
| 75 | + assertEquals(1L, result.getSenderId()); | ||
| 76 | + assertEquals(2L, result.getReceiverId()); | ||
| 77 | + assertEquals(0, result.getType()); | ||
| 78 | + assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus()); | ||
| 79 | + assertNotNull(result.getSendTime()); | ||
| 80 | + | ||
| 81 | + // 验证调用 | ||
| 82 | + verify(friendService).isFriend(1L, 2L); | ||
| 83 | + verify(sensitiveWordService).validateText(reqVO.getContent()); | ||
| 84 | + verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class)); | ||
| 85 | + // 验证推送给接收方和发送方 | ||
| 86 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); | ||
| 87 | + verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | ||
| 89 | } | 88 | } |
| 90 | 89 | ||
| 91 | @Test | 90 | @Test |
| @@ -96,7 +95,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -96,7 +95,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 96 | .id(100L).clientMessageId("test-uuid-001").senderId(1L).receiverId(2L) | 95 | .id(100L).clientMessageId("test-uuid-001").senderId(1L).receiverId(2L) |
| 97 | .type(0).content("{\"content\":\"你好\"}").status(0) | 96 | .type(0).content("{\"content\":\"你好\"}").status(0) |
| 98 | .sendTime(LocalDateTime.now()).build(); | 97 | .sendTime(LocalDateTime.now()).build(); |
| 99 | - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | 98 | + when(privateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) |
| 100 | .thenReturn(existingMessage); | 99 | .thenReturn(existingMessage); |
| 101 | 100 | ||
| 102 | // 调用 | 101 | // 调用 |
| @@ -105,16 +104,16 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -105,16 +104,16 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 105 | // 断言:返回已存在的消息 | 104 | // 断言:返回已存在的消息 |
| 106 | assertEquals(100L, result.getId()); | 105 | assertEquals(100L, result.getId()); |
| 107 | // 验证不会重复插入 | 106 | // 验证不会重复插入 |
| 108 | - verify(imPrivateMessageMapper, never()).insert(any(ImPrivateMessageDO.class)); | 107 | + verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class)); |
| 109 | } | 108 | } |
| 110 | 109 | ||
| 111 | @Test | 110 | @Test |
| 112 | public void testSendMessage_notFriend() { | 111 | public void testSendMessage_notFriend() { |
| 113 | // 准备 | 112 | // 准备 |
| 114 | ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); | 113 | ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); |
| 115 | - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | 114 | + when(privateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) |
| 116 | .thenReturn(null); | 115 | .thenReturn(null); |
| 117 | - when(imFriendService.isFriend(1L, 2L)).thenReturn(false); | 116 | + when(friendService.isFriend(1L, 2L)).thenReturn(false); |
| 118 | 117 | ||
| 119 | // 调用并断言 | 118 | // 调用并断言 |
| 120 | ServiceException exception = assertThrows(ServiceException.class, | 119 | ServiceException exception = assertThrows(ServiceException.class, |
| @@ -131,7 +130,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -131,7 +130,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 131 | ImPrivateMessageDO.builder().id(1L).senderId(1L).receiverId(2L).build(), | 130 | ImPrivateMessageDO.builder().id(1L).senderId(1L).receiverId(2L).build(), |
| 132 | ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L).build() | 131 | ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L).build() |
| 133 | ); | 132 | ); |
| 134 | - when(imPrivateMessageMapper.selectListByMinId(eq(1L), eq(0L), any(LocalDateTime.class), eq(100))) | 133 | + when(privateMessageMapper.selectListByMinId(eq(1L), eq(0L), any(LocalDateTime.class), eq(100))) |
| 135 | .thenReturn(mockMessages); | 134 | .thenReturn(mockMessages); |
| 136 | 135 | ||
| 137 | // 调用 | 136 | // 调用 |
| @@ -153,58 +152,67 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -153,58 +152,67 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 153 | 152 | ||
| 154 | @Test | 153 | @Test |
| 155 | public void testReadMessages_success() { | 154 | public void testReadMessages_success() { |
| 156 | - try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 157 | - springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) | ||
| 158 | - .thenReturn(privateMessageService); | ||
| 159 | - | ||
| 160 | - // 准备:mock 未读消息列表 | ||
| 161 | - List<ImPrivateMessageDO> unreadMessages = List.of( | ||
| 162 | - ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L) | ||
| 163 | - .status(ImMessageStatusEnum.UNREAD.getStatus()).build(), | ||
| 164 | - ImPrivateMessageDO.builder().id(2L).senderId(2L).receiverId(1L) | ||
| 165 | - .status(ImMessageStatusEnum.UNREAD.getStatus()).build() | ||
| 166 | - ); | ||
| 167 | - when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L, | ||
| 168 | - ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages); | ||
| 169 | - | ||
| 170 | - // 调用 | ||
| 171 | - privateMessageService.readPrivateMessages(1L, 2L); | ||
| 172 | - | ||
| 173 | - // 断言:更新了消息状态 + 发送了 READ 和 RECEIPT 事件 | ||
| 174 | - verify(imPrivateMessageMapper).updateByIdsAndStatus(eq(List.of(1L, 2L)), | ||
| 175 | - eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class)); | ||
| 176 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 177 | - } | 155 | + // 准备:mock 未读消息(id=1 和 id=5,maxReadId 应为 5) |
| 156 | + List<ImPrivateMessageDO> unreadMessages = List.of( | ||
| 157 | + ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L) | ||
| 158 | + .status(ImMessageStatusEnum.UNREAD.getStatus()).build(), | ||
| 159 | + ImPrivateMessageDO.builder().id(5L).senderId(2L).receiverId(1L) | ||
| 160 | + .status(ImMessageStatusEnum.UNREAD.getStatus()).build() | ||
| 161 | + ); | ||
| 162 | + when(privateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L, | ||
| 163 | + ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages); | ||
| 164 | + | ||
| 165 | + // 调用 | ||
| 166 | + privateMessageService.readPrivateMessages(1L, 2L); | ||
| 167 | + | ||
| 168 | + // 断言:更新了消息状态 | ||
| 169 | + verify(privateMessageMapper).updateByIdsAndStatus(eq(List.of(1L, 5L)), | ||
| 170 | + eq(ImMessageStatusEnum.UNREAD.getStatus()), any(ImPrivateMessageDO.class)); | ||
| 171 | + | ||
| 172 | + // 断言:发送了 READ + RECEIPT 事件,payload 字段正确 | ||
| 173 | + ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class); | ||
| 174 | + ArgumentCaptor<ImPrivateMessageDTO> contentCaptor = ArgumentCaptor.forClass(ImPrivateMessageDTO.class); | ||
| 175 | + verify(imWebSocketService, times(2)).sendPrivateMessageAsync( | ||
| 176 | + userCaptor.capture(), contentCaptor.capture()); | ||
| 177 | + | ||
| 178 | + // 第一次:发给自己的 READ 事件 | ||
| 179 | + assertEquals(1L, userCaptor.getAllValues().get(0)); | ||
| 180 | + ImPrivateMessageDTO readPayload = contentCaptor.getAllValues().get(0); | ||
| 181 | + assertEquals(ImMessageTypeEnum.READ.getType(), readPayload.getType()); | ||
| 182 | + assertEquals(1L, readPayload.getSenderId()); | ||
| 183 | + assertEquals(2L, readPayload.getReceiverId()); | ||
| 184 | + assertEquals(5L, readPayload.getId(), "READ id 应为 maxReadId"); | ||
| 185 | + | ||
| 186 | + // 第二次:发给对方的 RECEIPT 事件 | ||
| 187 | + assertEquals(2L, userCaptor.getAllValues().get(1)); | ||
| 188 | + ImPrivateMessageDTO receiptPayload = contentCaptor.getAllValues().get(1); | ||
| 189 | + assertEquals(ImMessageTypeEnum.RECEIPT.getType(), receiptPayload.getType()); | ||
| 190 | + assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为 maxReadId"); | ||
| 178 | } | 191 | } |
| 179 | 192 | ||
| 180 | // ========== 撤回测试 ========== | 193 | // ========== 撤回测试 ========== |
| 181 | 194 | ||
| 182 | @Test | 195 | @Test |
| 183 | public void testRecallMessage_success() { | 196 | public void testRecallMessage_success() { |
| 184 | - try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 185 | - springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) | ||
| 186 | - .thenReturn(privateMessageService); | ||
| 187 | - | ||
| 188 | - // 准备 | ||
| 189 | - ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 190 | - .id(10L).senderId(1L).receiverId(2L) | ||
| 191 | - .status(ImMessageStatusEnum.UNREAD.getStatus()) | ||
| 192 | - .sendTime(LocalDateTime.now()).build(); // 刚发送,5 分钟内 | ||
| 193 | - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 194 | - when(imPrivateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1); | ||
| 195 | - when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenReturn(1); | ||
| 196 | - | ||
| 197 | - // 调用 | ||
| 198 | - ImPrivateMessageDO result = privateMessageService.recallPrivateMessage(1L, 10L); | ||
| 199 | - | ||
| 200 | - // 断言:返回 TIP_TEXT 消息 | ||
| 201 | - assertNotNull(result); | ||
| 202 | - // 验证:更新原消息状态 + 插入 tipMessage | ||
| 203 | - verify(imPrivateMessageMapper).updateById(any(ImPrivateMessageDO.class)); | ||
| 204 | - verify(imPrivateMessageMapper).insert(any(ImPrivateMessageDO.class)); | ||
| 205 | - // 验证推送了消息(给接收方和发送方) | ||
| 206 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 207 | - } | 197 | + // 准备 |
| 198 | + ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 199 | + .id(10L).senderId(1L).receiverId(2L) | ||
| 200 | + .status(ImMessageStatusEnum.UNREAD.getStatus()) | ||
| 201 | + .sendTime(LocalDateTime.now()).build(); // 刚发送,5 分钟内 | ||
| 202 | + when(privateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 203 | + when(privateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1); | ||
| 204 | + when(privateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenReturn(1); | ||
| 205 | + | ||
| 206 | + // 调用 | ||
| 207 | + ImPrivateMessageDO result = privateMessageService.recallPrivateMessage(1L, 10L); | ||
| 208 | + | ||
| 209 | + // 断言:返回 TIP_TEXT 消息 | ||
| 210 | + assertNotNull(result); | ||
| 211 | + // 验证:更新原消息状态 + 插入 tipMessage | ||
| 212 | + verify(privateMessageMapper).updateById(any(ImPrivateMessageDO.class)); | ||
| 213 | + verify(privateMessageMapper).insert(any(ImPrivateMessageDO.class)); | ||
| 214 | + // 验证推送了消息(给接收方和发送方) | ||
| 215 | + verify(imWebSocketService, times(2)).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); | ||
| 208 | } | 216 | } |
| 209 | 217 | ||
| 210 | @Test | 218 | @Test |
| @@ -214,7 +222,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -214,7 +222,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 214 | .id(10L).senderId(2L).receiverId(1L) | 222 | .id(10L).senderId(2L).receiverId(1L) |
| 215 | .status(ImMessageStatusEnum.UNREAD.getStatus()) | 223 | .status(ImMessageStatusEnum.UNREAD.getStatus()) |
| 216 | .sendTime(LocalDateTime.now()).build(); | 224 | .sendTime(LocalDateTime.now()).build(); |
| 217 | - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | 225 | + when(privateMessageMapper.selectById(10L)).thenReturn(message); |
| 218 | 226 | ||
| 219 | // 调用并断言 | 227 | // 调用并断言 |
| 220 | ServiceException exception = assertThrows(ServiceException.class, | 228 | ServiceException exception = assertThrows(ServiceException.class, |
| @@ -229,7 +237,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -229,7 +237,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 229 | .id(10L).senderId(1L).receiverId(2L) | 237 | .id(10L).senderId(1L).receiverId(2L) |
| 230 | .status(ImMessageStatusEnum.RECALL.getStatus()) | 238 | .status(ImMessageStatusEnum.RECALL.getStatus()) |
| 231 | .sendTime(LocalDateTime.now()).build(); | 239 | .sendTime(LocalDateTime.now()).build(); |
| 232 | - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | 240 | + when(privateMessageMapper.selectById(10L)).thenReturn(message); |
| 233 | 241 | ||
| 234 | // 调用并断言 | 242 | // 调用并断言 |
| 235 | ServiceException exception = assertThrows(ServiceException.class, | 243 | ServiceException exception = assertThrows(ServiceException.class, |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/message/ImWebSocketEventTest.java deleted
| 1 | -package cn.iocoder.yudao.module.im.service.message; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 4 | -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 5 | -import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; | ||
| 6 | -import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 7 | -import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; | ||
| 8 | -import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 9 | -import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | ||
| 10 | -import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | ||
| 11 | -import cn.iocoder.yudao.module.im.service.sensitiveword.ImSensitiveWordService; | ||
| 12 | -import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | ||
| 13 | -import org.junit.jupiter.api.Test; | ||
| 14 | -import org.mockito.ArgumentCaptor; | ||
| 15 | -import org.mockito.InjectMocks; | ||
| 16 | -import org.mockito.Mock; | ||
| 17 | - | ||
| 18 | -import java.util.List; | ||
| 19 | - | ||
| 20 | -import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 21 | -import static org.junit.jupiter.api.Assertions.assertNotNull; | ||
| 22 | -import static org.mockito.ArgumentMatchers.*; | ||
| 23 | -import static org.mockito.Mockito.*; | ||
| 24 | - | ||
| 25 | -/** | ||
| 26 | - * IM WebSocket 事件单元测试 | ||
| 27 | - * <p> | ||
| 28 | - * 验证各事件的 payload 字段是否正确 | ||
| 29 | - * | ||
| 30 | - * @author 芋道源码 | ||
| 31 | - */ | ||
| 32 | -public class ImWebSocketEventTest extends BaseMockitoUnitTest { | ||
| 33 | - | ||
| 34 | - @InjectMocks | ||
| 35 | - private ImPrivateMessageServiceImpl privateMessageService; | ||
| 36 | - | ||
| 37 | - @Mock | ||
| 38 | - private ImPrivateMessageMapper imPrivateMessageMapper; | ||
| 39 | - @Mock | ||
| 40 | - private ImFriendService imFriendService; | ||
| 41 | - @Mock | ||
| 42 | - private ImSensitiveWordService imSensitiveWordService; | ||
| 43 | - @Mock | ||
| 44 | - private WebSocketMessageSender webSocketMessageSender; | ||
| 45 | - | ||
| 46 | - @Test | ||
| 47 | - public void testSendMessage_websocketEvent() { | ||
| 48 | - try (var springUtilMockedStatic = mockStatic(cn.hutool.extra.spring.SpringUtil.class)) { | ||
| 49 | - springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) | ||
| 50 | - .thenReturn(privateMessageService); | ||
| 51 | - | ||
| 52 | - // 准备 | ||
| 53 | - ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); | ||
| 54 | - reqVO.setClientMessageId("evt-uuid"); | ||
| 55 | - reqVO.setReceiverId(2L); | ||
| 56 | - reqVO.setType(0); | ||
| 57 | - reqVO.setContent("{\"content\":\"事件测试\"}"); | ||
| 58 | - | ||
| 59 | - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "evt-uuid")).thenReturn(null); | ||
| 60 | - when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenAnswer(invocation -> { | ||
| 61 | - ImPrivateMessageDO msg = invocation.getArgument(0); | ||
| 62 | - msg.setId(99L); | ||
| 63 | - return 1; | ||
| 64 | - }); | ||
| 65 | - | ||
| 66 | - // 调用 | ||
| 67 | - privateMessageService.sendPrivateMessage(1L, reqVO); | ||
| 68 | - | ||
| 69 | - // 捕获 WebSocket 调用 | ||
| 70 | - ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); | ||
| 71 | - ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class); | ||
| 72 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), | ||
| 73 | - typeCaptor.capture(), contentCaptor.capture()); | ||
| 74 | - | ||
| 75 | - // 验证消息事件类型:统一为 im-private-message | ||
| 76 | - assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(0)); | ||
| 77 | - assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(1)); | ||
| 78 | - | ||
| 79 | - // 验证 payload 是 DTO 类型 | ||
| 80 | - ImPrivateMessageDTO payload = (ImPrivateMessageDTO) contentCaptor.getAllValues().get(0); | ||
| 81 | - assertNotNull(payload.getId(), "id 不应为空"); | ||
| 82 | - assertEquals(1L, payload.getSenderId()); | ||
| 83 | - assertEquals(2L, payload.getReceiverId()); | ||
| 84 | - } | ||
| 85 | - } | ||
| 86 | - | ||
| 87 | - @Test | ||
| 88 | - public void testReadMessage_readAndReceiptEvents() { | ||
| 89 | - try (var springUtilMockedStatic = mockStatic(cn.hutool.extra.spring.SpringUtil.class)) { | ||
| 90 | - springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) | ||
| 91 | - .thenReturn(privateMessageService); | ||
| 92 | - | ||
| 93 | - // 准备:mock 未读消息(id=1 和 id=5,maxReadId 应为 5) | ||
| 94 | - List<ImPrivateMessageDO> unreadMessages = List.of( | ||
| 95 | - ImPrivateMessageDO.builder().id(1L).senderId(2L).receiverId(1L) | ||
| 96 | - .status(ImMessageStatusEnum.UNREAD.getStatus()).build(), | ||
| 97 | - ImPrivateMessageDO.builder().id(5L).senderId(2L).receiverId(1L) | ||
| 98 | - .status(ImMessageStatusEnum.UNREAD.getStatus()).build() | ||
| 99 | - ); | ||
| 100 | - when(imPrivateMessageMapper.selectListBySenderIdAndReceiverIdAndStatus(2L, 1L, | ||
| 101 | - ImMessageStatusEnum.UNREAD.getStatus())).thenReturn(unreadMessages); | ||
| 102 | - | ||
| 103 | - // 调用 | ||
| 104 | - privateMessageService.readPrivateMessages(1L, 2L); | ||
| 105 | - | ||
| 106 | - // 捕获 | ||
| 107 | - ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); | ||
| 108 | - ArgumentCaptor<Long> userCaptor = ArgumentCaptor.forClass(Long.class); | ||
| 109 | - ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class); | ||
| 110 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), userCaptor.capture(), | ||
| 111 | - typeCaptor.capture(), contentCaptor.capture()); | ||
| 112 | - | ||
| 113 | - // 第一次:发给自己的 READ 事件(统一 type = im-private-message) | ||
| 114 | - assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(0)); | ||
| 115 | - assertEquals(1L, userCaptor.getAllValues().get(0)); | ||
| 116 | - ImPrivateMessageDTO readPayload = (ImPrivateMessageDTO) contentCaptor.getAllValues().get(0); | ||
| 117 | - assertEquals(ImMessageTypeEnum.READ.getType(), readPayload.getType()); | ||
| 118 | - assertEquals(1L, readPayload.getSenderId()); | ||
| 119 | - assertEquals(2L, readPayload.getReceiverId()); | ||
| 120 | - assertEquals(5L, readPayload.getId(), "READ id 应为 maxReadId"); | ||
| 121 | - | ||
| 122 | - // 第二次:发给对方的 RECEIPT 事件(统一 type = im-private-message) | ||
| 123 | - assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(1)); | ||
| 124 | - assertEquals(2L, userCaptor.getAllValues().get(1)); | ||
| 125 | - ImPrivateMessageDTO receiptPayload = (ImPrivateMessageDTO) contentCaptor.getAllValues().get(1); | ||
| 126 | - assertEquals(ImMessageTypeEnum.RECEIPT.getType(), receiptPayload.getType()); | ||
| 127 | - assertEquals(1L, receiptPayload.getSenderId()); | ||
| 128 | - assertEquals(2L, receiptPayload.getReceiverId()); | ||
| 129 | - assertEquals(5L, receiptPayload.getId(), "RECEIPT id 应为 maxReadId"); | ||
| 130 | - } | ||
| 131 | - } | ||
| 132 | - | ||
| 133 | - @Test | ||
| 134 | - public void testRecallMessage_tipTextEvent() { | ||
| 135 | - try (var springUtilMockedStatic = mockStatic(cn.hutool.extra.spring.SpringUtil.class)) { | ||
| 136 | - springUtilMockedStatic.when(() -> cn.hutool.extra.spring.SpringUtil.getBean(eq(ImPrivateMessageServiceImpl.class))) | ||
| 137 | - .thenReturn(privateMessageService); | ||
| 138 | - | ||
| 139 | - // 准备 | ||
| 140 | - ImPrivateMessageDO message = ImPrivateMessageDO.builder() | ||
| 141 | - .id(10L).senderId(1L).receiverId(2L).status(0) | ||
| 142 | - .sendTime(java.time.LocalDateTime.now()).build(); | ||
| 143 | - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | ||
| 144 | - when(imPrivateMessageMapper.updateById(any(ImPrivateMessageDO.class))).thenReturn(1); | ||
| 145 | - when(imPrivateMessageMapper.insert(any(ImPrivateMessageDO.class))).thenReturn(1); | ||
| 146 | - | ||
| 147 | - // 调用 | ||
| 148 | - privateMessageService.recallPrivateMessage(1L, 10L); | ||
| 149 | - | ||
| 150 | - // 捕获 | ||
| 151 | - ArgumentCaptor<String> typeCaptor = ArgumentCaptor.forClass(String.class); | ||
| 152 | - ArgumentCaptor<Object> contentCaptor = ArgumentCaptor.forClass(Object.class); | ||
| 153 | - verify(webSocketMessageSender, times(2)).sendObject(anyInt(), anyLong(), | ||
| 154 | - typeCaptor.capture(), contentCaptor.capture()); | ||
| 155 | - | ||
| 156 | - // 验证:推送的是 TIP_TEXT 消息(统一 type = im-private-message) | ||
| 157 | - assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(0)); | ||
| 158 | - assertEquals(ImPrivateMessageDTO.TYPE, typeCaptor.getAllValues().get(1)); | ||
| 159 | - } | ||
| 160 | - } | ||
| 161 | - | ||
| 162 | -} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/sensitiveword/ImSensitiveWordServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.sensitiveword; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; | ||
| 4 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 5 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 6 | +import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper; | ||
| 8 | +import org.junit.jupiter.api.BeforeEach; | ||
| 9 | +import org.junit.jupiter.api.Test; | ||
| 10 | +import org.mockito.InjectMocks; | ||
| 11 | +import org.mockito.Mock; | ||
| 12 | + | ||
| 13 | +import java.util.List; | ||
| 14 | + | ||
| 15 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED; | ||
| 16 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 17 | +import static org.mockito.Mockito.*; | ||
| 18 | + | ||
| 19 | +/** | ||
| 20 | + * IM 敏感词 Service 单元测试 | ||
| 21 | + * | ||
| 22 | + * @author 芋道源码 | ||
| 23 | + */ | ||
| 24 | +public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest { | ||
| 25 | + | ||
| 26 | + @InjectMocks | ||
| 27 | + private ImSensitiveWordServiceImpl sensitiveWordService; | ||
| 28 | + | ||
| 29 | + @Mock | ||
| 30 | + private ImSensitiveWordMapper imSensitiveWordMapper; | ||
| 31 | + | ||
| 32 | + @BeforeEach | ||
| 33 | + public void setUp() { | ||
| 34 | + // 模拟 @PostConstruct 初始化:加载 2 个启用的敏感词 | ||
| 35 | + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) | ||
| 36 | + .thenReturn(List.of( | ||
| 37 | + ImSensitiveWordDO.builder().id(1L).word("badword") | ||
| 38 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(), | ||
| 39 | + ImSensitiveWordDO.builder().id(2L).word("违禁词") | ||
| 40 | + .status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 41 | + )); | ||
| 42 | + sensitiveWordService.init(); | ||
| 43 | + } | ||
| 44 | + | ||
| 45 | + @Test | ||
| 46 | + public void testValidateText_null() { | ||
| 47 | + // null 直接返回,不抛异常 | ||
| 48 | + assertDoesNotThrow(() -> sensitiveWordService.validateText(null)); | ||
| 49 | + } | ||
| 50 | + | ||
| 51 | + @Test | ||
| 52 | + public void testValidateText_empty() { | ||
| 53 | + assertDoesNotThrow(() -> sensitiveWordService.validateText("")); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Test | ||
| 57 | + public void testValidateText_clean() { | ||
| 58 | + // 正常文本不应命中 | ||
| 59 | + assertDoesNotThrow(() -> sensitiveWordService.validateText("hello world")); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @Test | ||
| 63 | + public void testValidateText_hitEnglish() { | ||
| 64 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 65 | + () -> sensitiveWordService.validateText("this contains badword here")); | ||
| 66 | + assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode()); | ||
| 67 | + } | ||
| 68 | + | ||
| 69 | + @Test | ||
| 70 | + public void testValidateText_hitChinese() { | ||
| 71 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 72 | + () -> sensitiveWordService.validateText("这条消息里有违禁词哦")); | ||
| 73 | + assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode()); | ||
| 74 | + } | ||
| 75 | + | ||
| 76 | + @Test | ||
| 77 | + public void testRefresh_reloadsFromMapper() { | ||
| 78 | + // 刷新前:第一次调用 mapper | ||
| 79 | + verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 80 | + | ||
| 81 | + // 准备:新列表不含原词 | ||
| 82 | + when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())) | ||
| 83 | + .thenReturn(List.of( | ||
| 84 | + ImSensitiveWordDO.builder().id(3L).word("new").status(CommonStatusEnum.ENABLE.getStatus()).build() | ||
| 85 | + )); | ||
| 86 | + | ||
| 87 | + // 调用:刷新 | ||
| 88 | + sensitiveWordService.refresh(); | ||
| 89 | + | ||
| 90 | + // 断言:原敏感词不再命中,新词命中 | ||
| 91 | + assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword")); | ||
| 92 | + assertThrows(ServiceException.class, | ||
| 93 | + () -> sensitiveWordService.validateText("contains new")); | ||
| 94 | + verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 95 | + } | ||
| 96 | + | ||
| 97 | +} |
yudao-module-im/yudao-module-im-biz/src/test/java/cn/iocoder/yudao/module/im/service/websocket/ImWebSocketServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.websocket; | ||
| 2 | + | ||
| 3 | +import cn.hutool.extra.spring.SpringUtil; | ||
| 4 | +import cn.iocoder.yudao.framework.common.enums.UserTypeEnum; | ||
| 5 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 6 | +import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | ||
| 7 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImGroupMessageDTO; | ||
| 8 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | ||
| 9 | +import org.junit.jupiter.api.AfterEach; | ||
| 10 | +import org.junit.jupiter.api.Test; | ||
| 11 | +import org.mockito.InjectMocks; | ||
| 12 | +import org.mockito.Mock; | ||
| 13 | +import org.mockito.MockedStatic; | ||
| 14 | +import org.springframework.transaction.support.TransactionSynchronization; | ||
| 15 | +import org.springframework.transaction.support.TransactionSynchronizationManager; | ||
| 16 | + | ||
| 17 | +import java.util.List; | ||
| 18 | + | ||
| 19 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 20 | +import static org.mockito.ArgumentMatchers.eq; | ||
| 21 | +import static org.mockito.Mockito.*; | ||
| 22 | + | ||
| 23 | +/** | ||
| 24 | + * IM WebSocket 推送 Service 单元测试 | ||
| 25 | + * | ||
| 26 | + * @author 芋道源码 | ||
| 27 | + */ | ||
| 28 | +public class ImWebSocketServiceImplTest extends BaseMockitoUnitTest { | ||
| 29 | + | ||
| 30 | + @InjectMocks | ||
| 31 | + private ImWebSocketServiceImpl imWebSocketService; | ||
| 32 | + | ||
| 33 | + @Mock | ||
| 34 | + private WebSocketMessageSender webSocketMessageSender; | ||
| 35 | + | ||
| 36 | + @AfterEach | ||
| 37 | + public void tearDown() { | ||
| 38 | + // 清理事务同步上下文,避免串扰其它用例 | ||
| 39 | + if (TransactionSynchronizationManager.isSynchronizationActive()) { | ||
| 40 | + TransactionSynchronizationManager.clearSynchronization(); | ||
| 41 | + } | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + // ========== sendPrivateMessageAsync ========== | ||
| 45 | + | ||
| 46 | + @Test | ||
| 47 | + public void testSendPrivateMessageAsync_noTransactionSendsImmediately() { | ||
| 48 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 49 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class))) | ||
| 50 | + .thenReturn(imWebSocketService); | ||
| 51 | + | ||
| 52 | + // 准备 | ||
| 53 | + ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L); | ||
| 54 | + | ||
| 55 | + // 调用:无事务,应立即发送 | ||
| 56 | + imWebSocketService.sendPrivateMessageAsync(2L, dto); | ||
| 57 | + | ||
| 58 | + // 断言 | ||
| 59 | + verify(webSocketMessageSender).sendObject( | ||
| 60 | + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImPrivateMessageDTO.TYPE), eq(dto)); | ||
| 61 | + } | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + @Test | ||
| 65 | + public void testSendPrivateMessageAsync_inTransactionDeferredUntilCommit() { | ||
| 66 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 67 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class))) | ||
| 68 | + .thenReturn(imWebSocketService); | ||
| 69 | + | ||
| 70 | + // 准备:开启事务同步 | ||
| 71 | + TransactionSynchronizationManager.initSynchronization(); | ||
| 72 | + try { | ||
| 73 | + ImPrivateMessageDTO dto = new ImPrivateMessageDTO().setSenderId(1L).setReceiverId(2L); | ||
| 74 | + | ||
| 75 | + // 调用 | ||
| 76 | + imWebSocketService.sendPrivateMessageAsync(2L, dto); | ||
| 77 | + | ||
| 78 | + // 断言:事务未提交,未推送 | ||
| 79 | + verify(webSocketMessageSender, never()).sendObject(anyInt(), anyLong(), anyString(), any()); | ||
| 80 | + | ||
| 81 | + // 模拟事务提交 | ||
| 82 | + List<TransactionSynchronization> syncs = | ||
| 83 | + TransactionSynchronizationManager.getSynchronizations(); | ||
| 84 | + assertEquals(1, syncs.size()); | ||
| 85 | + syncs.forEach(TransactionSynchronization::afterCommit); | ||
| 86 | + | ||
| 87 | + // 断言:提交后推送 | ||
| 88 | + verify(webSocketMessageSender).sendObject( | ||
| 89 | + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImPrivateMessageDTO.TYPE), eq(dto)); | ||
| 90 | + } finally { | ||
| 91 | + TransactionSynchronizationManager.clear(); | ||
| 92 | + } | ||
| 93 | + } | ||
| 94 | + } | ||
| 95 | + | ||
| 96 | + // ========== sendGroupMessageAsync ========== | ||
| 97 | + | ||
| 98 | + @Test | ||
| 99 | + public void testSendGroupMessageAsync_fanOutToAllUsers() { | ||
| 100 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 101 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class))) | ||
| 102 | + .thenReturn(imWebSocketService); | ||
| 103 | + | ||
| 104 | + ImGroupMessageDTO dto = new ImGroupMessageDTO(); | ||
| 105 | + dto.setGroupId(10L); | ||
| 106 | + dto.setSenderId(1L); | ||
| 107 | + | ||
| 108 | + imWebSocketService.sendGroupMessageAsync(List.of(1L, 2L, 3L), dto); | ||
| 109 | + | ||
| 110 | + verify(webSocketMessageSender).sendObject( | ||
| 111 | + eq(UserTypeEnum.ADMIN.getValue()), eq(1L), eq(ImGroupMessageDTO.TYPE), eq(dto)); | ||
| 112 | + verify(webSocketMessageSender).sendObject( | ||
| 113 | + eq(UserTypeEnum.ADMIN.getValue()), eq(2L), eq(ImGroupMessageDTO.TYPE), eq(dto)); | ||
| 114 | + verify(webSocketMessageSender).sendObject( | ||
| 115 | + eq(UserTypeEnum.ADMIN.getValue()), eq(3L), eq(ImGroupMessageDTO.TYPE), eq(dto)); | ||
| 116 | + } | ||
| 117 | + } | ||
| 118 | + | ||
| 119 | + @Test | ||
| 120 | + public void testSendGroupMessageAsync_senderExceptionDoesNotBreakOthers() { | ||
| 121 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 122 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class))) | ||
| 123 | + .thenReturn(imWebSocketService); | ||
| 124 | + | ||
| 125 | + ImGroupMessageDTO dto = new ImGroupMessageDTO(); | ||
| 126 | + dto.setGroupId(10L); | ||
| 127 | + // 给 1 号用户推送时抛异常,不能影响 2/3 号 | ||
| 128 | + doThrow(new RuntimeException("user offline")) | ||
| 129 | + .when(webSocketMessageSender).sendObject(anyInt(), eq(1L), anyString(), any()); | ||
| 130 | + | ||
| 131 | + imWebSocketService.sendGroupMessageAsync(List.of(1L, 2L, 3L), dto); | ||
| 132 | + | ||
| 133 | + // 2L 和 3L 也都被推送 | ||
| 134 | + verify(webSocketMessageSender).sendObject(anyInt(), eq(2L), anyString(), any()); | ||
| 135 | + verify(webSocketMessageSender).sendObject(anyInt(), eq(3L), anyString(), any()); | ||
| 136 | + } | ||
| 137 | + } | ||
| 138 | + | ||
| 139 | + @Test | ||
| 140 | + public void testSendGroupMessageAsync_singleUserDefaultOverload() { | ||
| 141 | + try (MockedStatic<SpringUtil> springUtilMockedStatic = mockStatic(SpringUtil.class)) { | ||
| 142 | + springUtilMockedStatic.when(() -> SpringUtil.getBean(eq(ImWebSocketServiceImpl.class))) | ||
| 143 | + .thenReturn(imWebSocketService); | ||
| 144 | + | ||
| 145 | + ImGroupMessageDTO dto = new ImGroupMessageDTO(); | ||
| 146 | + dto.setGroupId(10L); | ||
| 147 | + | ||
| 148 | + imWebSocketService.sendGroupMessageAsync(42L, dto); | ||
| 149 | + | ||
| 150 | + verify(webSocketMessageSender).sendObject( | ||
| 151 | + eq(UserTypeEnum.ADMIN.getValue()), eq(42L), eq(ImGroupMessageDTO.TYPE), eq(dto)); | ||
| 152 | + } | ||
| 153 | + } | ||
| 154 | + | ||
| 155 | +} |
yudao-module-im/yudao-module-im-biz/src/test/resources/sql/clean.sql
yudao-module-im/yudao-module-im-biz/src/test/resources/sql/create_tables.sql
| @@ -15,3 +15,80 @@ CREATE TABLE IF NOT EXISTS "im_private_message" ( | @@ -15,3 +15,80 @@ CREATE TABLE IF NOT EXISTS "im_private_message" ( | ||
| 15 | "tenant_id" bigint NOT NULL DEFAULT 0, | 15 | "tenant_id" bigint NOT NULL DEFAULT 0, |
| 16 | PRIMARY KEY ("id") | 16 | PRIMARY KEY ("id") |
| 17 | ) COMMENT 'IM 私聊消息表'; | 17 | ) COMMENT 'IM 私聊消息表'; |
| 18 | + | ||
| 19 | +CREATE TABLE IF NOT EXISTS "im_group_message" ( | ||
| 20 | + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', | ||
| 21 | + "client_message_id" varchar(64) DEFAULT NULL COMMENT '客户端消息编号', | ||
| 22 | + "sender_id" bigint NOT NULL COMMENT '发送人编号', | ||
| 23 | + "group_id" bigint NOT NULL COMMENT '群编号', | ||
| 24 | + "type" tinyint NOT NULL COMMENT '消息类型', | ||
| 25 | + "content" varchar(4096) DEFAULT NULL COMMENT '消息内容', | ||
| 26 | + "status" tinyint NOT NULL COMMENT '消息状态', | ||
| 27 | + "send_time" timestamp NOT NULL COMMENT '发送时间', | ||
| 28 | + "receiver_user_ids" varchar(1024) DEFAULT NULL COMMENT '定向接收用户编号列表', | ||
| 29 | + "at_user_ids" varchar(1024) DEFAULT NULL COMMENT '@ 目标用户编号列表', | ||
| 30 | + "receipt_status" tinyint NOT NULL DEFAULT 0 COMMENT '回执状态', | ||
| 31 | + "creator" varchar(64) DEFAULT '', | ||
| 32 | + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 33 | + "updater" varchar(64) DEFAULT '', | ||
| 34 | + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 35 | + "deleted" bit NOT NULL DEFAULT FALSE, | ||
| 36 | + "tenant_id" bigint NOT NULL DEFAULT 0, | ||
| 37 | + PRIMARY KEY ("id") | ||
| 38 | +) COMMENT 'IM 群聊消息表'; | ||
| 39 | + | ||
| 40 | +CREATE TABLE IF NOT EXISTS "im_group" ( | ||
| 41 | + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', | ||
| 42 | + "name" varchar(64) NOT NULL COMMENT '群名称', | ||
| 43 | + "owner_user_id" bigint NOT NULL COMMENT '群主用户编号', | ||
| 44 | + "avatar" varchar(512) DEFAULT NULL COMMENT '群头像', | ||
| 45 | + "notice" varchar(2048) DEFAULT NULL COMMENT '群公告', | ||
| 46 | + "banned" bit DEFAULT FALSE COMMENT '是否封禁', | ||
| 47 | + "banned_reason" varchar(512) DEFAULT NULL COMMENT '封禁原因', | ||
| 48 | + "banned_time" timestamp DEFAULT NULL COMMENT '封禁时间', | ||
| 49 | + "status" tinyint NOT NULL COMMENT '群状态', | ||
| 50 | + "dissolved_time" timestamp DEFAULT NULL COMMENT '解散时间', | ||
| 51 | + "creator" varchar(64) DEFAULT '', | ||
| 52 | + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 53 | + "updater" varchar(64) DEFAULT '', | ||
| 54 | + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 55 | + "deleted" bit NOT NULL DEFAULT FALSE, | ||
| 56 | + "tenant_id" bigint NOT NULL DEFAULT 0, | ||
| 57 | + PRIMARY KEY ("id") | ||
| 58 | +) COMMENT 'IM 群信息表'; | ||
| 59 | + | ||
| 60 | +CREATE TABLE IF NOT EXISTS "im_group_member" ( | ||
| 61 | + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', | ||
| 62 | + "group_id" bigint NOT NULL COMMENT '群编号', | ||
| 63 | + "user_id" bigint NOT NULL COMMENT '用户编号', | ||
| 64 | + "display_user_name" varchar(64) DEFAULT NULL COMMENT '组内显示名', | ||
| 65 | + "display_group_name" varchar(64) DEFAULT NULL COMMENT '群显示备注', | ||
| 66 | + "muted" bit DEFAULT FALSE COMMENT '是否免打扰', | ||
| 67 | + "status" tinyint NOT NULL COMMENT '成员状态', | ||
| 68 | + "join_time" timestamp DEFAULT NULL COMMENT '入群时间', | ||
| 69 | + "quit_time" timestamp DEFAULT NULL COMMENT '退群时间', | ||
| 70 | + "creator" varchar(64) DEFAULT '', | ||
| 71 | + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 72 | + "updater" varchar(64) DEFAULT '', | ||
| 73 | + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 74 | + "deleted" bit NOT NULL DEFAULT FALSE, | ||
| 75 | + "tenant_id" bigint NOT NULL DEFAULT 0, | ||
| 76 | + PRIMARY KEY ("id") | ||
| 77 | +) COMMENT 'IM 群成员表'; | ||
| 78 | + | ||
| 79 | +CREATE TABLE IF NOT EXISTS "im_friend" ( | ||
| 80 | + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY COMMENT '编号', | ||
| 81 | + "user_id" bigint NOT NULL COMMENT '用户编号', | ||
| 82 | + "friend_user_id" bigint NOT NULL COMMENT '好友用户编号', | ||
| 83 | + "muted" bit DEFAULT FALSE COMMENT '是否免打扰', | ||
| 84 | + "status" tinyint NOT NULL COMMENT '好友状态', | ||
| 85 | + "add_time" timestamp DEFAULT NULL COMMENT '添加好友时间', | ||
| 86 | + "delete_time" timestamp DEFAULT NULL COMMENT '删除好友时间', | ||
| 87 | + "creator" varchar(64) DEFAULT '', | ||
| 88 | + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 89 | + "updater" varchar(64) DEFAULT '', | ||
| 90 | + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 91 | + "deleted" bit NOT NULL DEFAULT FALSE, | ||
| 92 | + "tenant_id" bigint NOT NULL DEFAULT 0, | ||
| 93 | + PRIMARY KEY ("id") | ||
| 94 | +) COMMENT 'IM 好友关系表'; |