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 | 4 | import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum; |
| 5 | 5 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 6 | 6 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 7 | -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | |
| 8 | 7 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.group.ImGroupMessageSendReqVO; |
| 9 | 8 | import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupDO; |
| 10 | 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 | 15 | import cn.iocoder.yudao.module.im.service.group.ImGroupMemberService; |
| 17 | 16 | import cn.iocoder.yudao.module.im.service.group.ImGroupService; |
| 18 | 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 | 20 | import org.junit.jupiter.api.Test; |
| 20 | 21 | import org.mockito.InjectMocks; |
| 21 | 22 | import org.mockito.Mock; |
| 22 | 23 | import org.mockito.MockedStatic; |
| 23 | 24 | |
| 24 | 25 | import java.time.LocalDateTime; |
| 26 | +import java.util.Collection; | |
| 25 | 27 | import java.util.HashMap; |
| 26 | 28 | import java.util.List; |
| 27 | 29 | import java.util.Map; |
| ... | ... | @@ -52,7 +54,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 52 | 54 | @Mock |
| 53 | 55 | private GroupMessageReadRedisDAO groupMessageReadRedisDAO; |
| 54 | 56 | @Mock |
| 55 | - private WebSocketMessageSender webSocketMessageSender; | |
| 57 | + private ImWebSocketService imWebSocketService; | |
| 56 | 58 | |
| 57 | 59 | private ImGroupMessageSendReqVO buildSendReqVO() { |
| 58 | 60 | ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO(); |
| ... | ... | @@ -109,7 +111,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { |
| 109 | 111 | assertEquals(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus(), result.getReceiptStatus()); |
| 110 | 112 | |
| 111 | 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 | 575 | // 验证:更新原消息状态 + 插入 tipMessage |
| 572 | 576 | verify(groupMessageMapper).updateById(any(ImGroupMessageDO.class)); |
| 573 | 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 | 1 | package cn.iocoder.yudao.module.im.service.message; |
| 2 | 2 | |
| 3 | -import cn.hutool.extra.spring.SpringUtil; | |
| 4 | 3 | import cn.iocoder.yudao.framework.common.exception.ServiceException; |
| 5 | 4 | import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; |
| 6 | -import cn.iocoder.yudao.framework.websocket.core.sender.WebSocketMessageSender; | |
| 7 | 5 | import cn.iocoder.yudao.module.im.controller.admin.message.vo.privates.ImPrivateMessageSendReqVO; |
| 8 | 6 | import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; |
| 9 | 7 | import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; |
| 10 | 8 | import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; |
| 9 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | |
| 11 | 10 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 12 | 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 | 14 | import org.junit.jupiter.api.Test; |
| 15 | +import org.mockito.ArgumentCaptor; | |
| 14 | 16 | import org.mockito.InjectMocks; |
| 15 | 17 | import org.mockito.Mock; |
| 16 | -import org.mockito.MockedStatic; | |
| 17 | 18 | |
| 18 | 19 | import java.time.LocalDateTime; |
| 19 | 20 | import java.util.List; |
| ... | ... | @@ -34,13 +35,13 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 34 | 35 | private ImPrivateMessageServiceImpl privateMessageService; |
| 35 | 36 | |
| 36 | 37 | @Mock |
| 37 | - private ImPrivateMessageMapper imPrivateMessageMapper; | |
| 38 | + private ImPrivateMessageMapper privateMessageMapper; | |
| 38 | 39 | @Mock |
| 39 | - private ImFriendService imFriendService; | |
| 40 | + private ImFriendService friendService; | |
| 40 | 41 | @Mock |
| 41 | - private ImSensitiveWordService imSensitiveWordService; | |
| 42 | + private ImSensitiveWordService sensitiveWordService; | |
| 42 | 43 | @Mock |
| 43 | - private WebSocketMessageSender webSocketMessageSender; | |
| 44 | + private ImWebSocketService imWebSocketService; | |
| 44 | 45 | |
| 45 | 46 | private ImPrivateMessageSendReqVO buildSendReqVO() { |
| 46 | 47 | ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); |
| ... | ... | @@ -55,37 +56,35 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 55 | 56 | |
| 56 | 57 | @Test |
| 57 | 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 | 90 | @Test |
| ... | ... | @@ -96,7 +95,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 96 | 95 | .id(100L).clientMessageId("test-uuid-001").senderId(1L).receiverId(2L) |
| 97 | 96 | .type(0).content("{\"content\":\"你好\"}").status(0) |
| 98 | 97 | .sendTime(LocalDateTime.now()).build(); |
| 99 | - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | |
| 98 | + when(privateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | |
| 100 | 99 | .thenReturn(existingMessage); |
| 101 | 100 | |
| 102 | 101 | // 调用 |
| ... | ... | @@ -105,16 +104,16 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 105 | 104 | // 断言:返回已存在的消息 |
| 106 | 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 | 110 | @Test |
| 112 | 111 | public void testSendMessage_notFriend() { |
| 113 | 112 | // 准备 |
| 114 | 113 | ImPrivateMessageSendReqVO reqVO = buildSendReqVO(); |
| 115 | - when(imPrivateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | |
| 114 | + when(privateMessageMapper.selectBySenderIdAndClientMessageId(1L, "test-uuid-001")) | |
| 116 | 115 | .thenReturn(null); |
| 117 | - when(imFriendService.isFriend(1L, 2L)).thenReturn(false); | |
| 116 | + when(friendService.isFriend(1L, 2L)).thenReturn(false); | |
| 118 | 117 | |
| 119 | 118 | // 调用并断言 |
| 120 | 119 | ServiceException exception = assertThrows(ServiceException.class, |
| ... | ... | @@ -131,7 +130,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 131 | 130 | ImPrivateMessageDO.builder().id(1L).senderId(1L).receiverId(2L).build(), |
| 132 | 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 | 134 | .thenReturn(mockMessages); |
| 136 | 135 | |
| 137 | 136 | // 调用 |
| ... | ... | @@ -153,58 +152,67 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 153 | 152 | |
| 154 | 153 | @Test |
| 155 | 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 | 195 | @Test |
| 183 | 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 | 218 | @Test |
| ... | ... | @@ -214,7 +222,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 214 | 222 | .id(10L).senderId(2L).receiverId(1L) |
| 215 | 223 | .status(ImMessageStatusEnum.UNREAD.getStatus()) |
| 216 | 224 | .sendTime(LocalDateTime.now()).build(); |
| 217 | - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | |
| 225 | + when(privateMessageMapper.selectById(10L)).thenReturn(message); | |
| 218 | 226 | |
| 219 | 227 | // 调用并断言 |
| 220 | 228 | ServiceException exception = assertThrows(ServiceException.class, |
| ... | ... | @@ -229,7 +237,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { |
| 229 | 237 | .id(10L).senderId(1L).receiverId(2L) |
| 230 | 238 | .status(ImMessageStatusEnum.RECALL.getStatus()) |
| 231 | 239 | .sendTime(LocalDateTime.now()).build(); |
| 232 | - when(imPrivateMessageMapper.selectById(10L)).thenReturn(message); | |
| 240 | + when(privateMessageMapper.selectById(10L)).thenReturn(message); | |
| 233 | 241 | |
| 234 | 242 | // 调用并断言 |
| 235 | 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 | 15 | "tenant_id" bigint NOT NULL DEFAULT 0, |
| 16 | 16 | PRIMARY KEY ("id") |
| 17 | 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 好友关系表'; | ... | ... |