Commit 6b1a0cfce2c793e495c7512ed39d9cf212f729e0
1 parent
36c44105
✨ feat(im): 更多单元测试
Showing
19 changed files
with
1528 additions
and
457 deletions
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/friend/ImFriendRequestMapperTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.friend; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; | ||
| 5 | +import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum; | ||
| 6 | +import jakarta.annotation.Resource; | ||
| 7 | +import org.junit.jupiter.api.Test; | ||
| 8 | + | ||
| 9 | +import java.util.List; | ||
| 10 | + | ||
| 11 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 12 | + | ||
| 13 | +/** | ||
| 14 | + * IM 好友申请记录 Mapper 单元测试 | ||
| 15 | + * | ||
| 16 | + * @author 芋道源码 | ||
| 17 | + */ | ||
| 18 | +public class ImFriendRequestMapperTest extends BaseDbUnitTest { | ||
| 19 | + | ||
| 20 | + @Resource | ||
| 21 | + private ImFriendRequestMapper mapper; | ||
| 22 | + | ||
| 23 | + // ========== selectMyList ========== | ||
| 24 | + | ||
| 25 | + @Test | ||
| 26 | + public void testSelectMyList_bidirectional() { | ||
| 27 | + // 准备:1 既作为 from 又作为 to;3 是无关用户 | ||
| 28 | + mapper.insert(buildRequest(1L, 2L)); | ||
| 29 | + mapper.insert(buildRequest(2L, 1L)); | ||
| 30 | + mapper.insert(buildRequest(3L, 4L)); | ||
| 31 | + | ||
| 32 | + // 调用:lastRequestId=null 拉首页 | ||
| 33 | + List<ImFriendRequestDO> list = mapper.selectMyList(1L, null, 10); | ||
| 34 | + | ||
| 35 | + // 断言:双向 OR 命中两条,无关用户被排除 | ||
| 36 | + assertEquals(2, list.size()); | ||
| 37 | + list.forEach(r -> assertTrue(r.getFromUserId().equals(1L) || r.getToUserId().equals(1L))); | ||
| 38 | + } | ||
| 39 | + | ||
| 40 | + @Test | ||
| 41 | + public void testSelectMyList_cursorPaging() { | ||
| 42 | + // 准备:三条 1 相关的申请 | ||
| 43 | + ImFriendRequestDO r1 = buildRequest(1L, 2L); | ||
| 44 | + ImFriendRequestDO r2 = buildRequest(1L, 3L); | ||
| 45 | + ImFriendRequestDO r3 = buildRequest(1L, 4L); | ||
| 46 | + mapper.insert(r1); | ||
| 47 | + mapper.insert(r2); | ||
| 48 | + mapper.insert(r3); | ||
| 49 | + | ||
| 50 | + // 调用:lastRequestId = r3.id,拉 id < r3.id 的下一页 | ||
| 51 | + List<ImFriendRequestDO> next = mapper.selectMyList(1L, r3.getId(), 10); | ||
| 52 | + | ||
| 53 | + // 断言:仅含 r1 / r2,按 id 倒序 | ||
| 54 | + assertEquals(2, next.size()); | ||
| 55 | + assertEquals(r2.getId(), next.get(0).getId()); | ||
| 56 | + assertEquals(r1.getId(), next.get(1).getId()); | ||
| 57 | + } | ||
| 58 | + | ||
| 59 | + @Test | ||
| 60 | + public void testSelectMyList_limit() { | ||
| 61 | + // 准备:插入 3 条 | ||
| 62 | + for (int i = 0; i < 3; i++) { | ||
| 63 | + mapper.insert(buildRequest(1L, (long) (10 + i))); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + // 调用:limit = 2 | ||
| 67 | + List<ImFriendRequestDO> list = mapper.selectMyList(1L, null, 2); | ||
| 68 | + | ||
| 69 | + // 断言:手写 LIMIT 真生效 | ||
| 70 | + assertEquals(2, list.size()); | ||
| 71 | + } | ||
| 72 | + | ||
| 73 | + private static ImFriendRequestDO buildRequest(Long fromUserId, Long toUserId) { | ||
| 74 | + return new ImFriendRequestDO() | ||
| 75 | + .setFromUserId(fromUserId).setToUserId(toUserId) | ||
| 76 | + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/group/ImGroupMemberMapperTest.java deleted
| 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 | - // ========== selectQuitListByUserId ========== | ||
| 25 | - | ||
| 26 | - @Test | ||
| 27 | - public void testSelectQuitListByUserId_onlyDisabled() { | ||
| 28 | - // 准备:一条 ENABLE + 一条 DISABLE | ||
| 29 | - mapper.insert(buildMember(10L, 1L, CommonStatusEnum.ENABLE)); | ||
| 30 | - ImGroupMemberDO quit = buildMember(20L, 1L, CommonStatusEnum.DISABLE); | ||
| 31 | - quit.setQuitTime(LocalDateTime.now().minusDays(1)); | ||
| 32 | - mapper.insert(quit); | ||
| 33 | - | ||
| 34 | - // 调用:minQuitTime 为 null,不按时间过滤 | ||
| 35 | - List<ImGroupMemberDO> result = mapper.selectQuitListByUserId(1L, null); | ||
| 36 | - | ||
| 37 | - // 断言:只返回退群记录 | ||
| 38 | - assertEquals(1, result.size()); | ||
| 39 | - assertEquals(quit.getId(), result.get(0).getId()); | ||
| 40 | - } | ||
| 41 | - | ||
| 42 | - @Test | ||
| 43 | - public void testSelectQuitListByUserId_filterByMinQuitTime() { | ||
| 44 | - // 准备:两条退群记录,时间一早一晚 | ||
| 45 | - LocalDateTime old = LocalDateTime.now().minusDays(10); | ||
| 46 | - LocalDateTime recent = LocalDateTime.now().minusDays(1); | ||
| 47 | - ImGroupMemberDO oldQuit = buildMember(10L, 1L, CommonStatusEnum.DISABLE); | ||
| 48 | - oldQuit.setQuitTime(old); | ||
| 49 | - mapper.insert(oldQuit); | ||
| 50 | - ImGroupMemberDO recentQuit = buildMember(20L, 1L, CommonStatusEnum.DISABLE); | ||
| 51 | - recentQuit.setQuitTime(recent); | ||
| 52 | - mapper.insert(recentQuit); | ||
| 53 | - | ||
| 54 | - // 调用:minQuitTime = now - 5 天,只返回近期退群 | ||
| 55 | - List<ImGroupMemberDO> result = mapper.selectQuitListByUserId(1L, LocalDateTime.now().minusDays(5)); | ||
| 56 | - assertEquals(1, result.size()); | ||
| 57 | - assertEquals(recentQuit.getId(), result.get(0).getId()); | ||
| 58 | - | ||
| 59 | - // 调用:minQuitTime 为 null,不过滤 | ||
| 60 | - List<ImGroupMemberDO> all = mapper.selectQuitListByUserId(1L, null); | ||
| 61 | - assertEquals(2, all.size()); | ||
| 62 | - } | ||
| 63 | - | ||
| 64 | - // ========== updateByGroupIdAndUserIdsAndStatus ========== | ||
| 65 | - | ||
| 66 | - @Test | ||
| 67 | - public void testUpdateByGroupIdAndUserIdsAndStatus_onlyUpdatesOldStatusMatches() { | ||
| 68 | - // 准备:u1 ENABLE,u2 DISABLE(已退群,不应更新) | ||
| 69 | - ImGroupMemberDO u1 = buildMember(10L, 1L, CommonStatusEnum.ENABLE); | ||
| 70 | - mapper.insert(u1); | ||
| 71 | - ImGroupMemberDO u2 = buildMember(10L, 2L, CommonStatusEnum.DISABLE); | ||
| 72 | - u2.setQuitTime(LocalDateTime.now().minusDays(5)); | ||
| 73 | - mapper.insert(u2); | ||
| 74 | - | ||
| 75 | - // 调用:把 ENABLE 状态的成员移除 | ||
| 76 | - LocalDateTime now = LocalDateTime.now(); | ||
| 77 | - mapper.updateByGroupIdAndUserIdsAndStatus(10L, List.of(1L, 2L), | ||
| 78 | - CommonStatusEnum.ENABLE.getStatus(), | ||
| 79 | - new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus()).setQuitTime(now)); | ||
| 80 | - | ||
| 81 | - // 断言:u1 被更新 | ||
| 82 | - ImGroupMemberDO after1 = mapper.selectById(u1.getId()); | ||
| 83 | - assertEquals(CommonStatusEnum.DISABLE.getStatus(), after1.getStatus()); | ||
| 84 | - assertNotNull(after1.getQuitTime()); | ||
| 85 | - // 断言:u2 的 quitTime 不变(仍为 5 天前) | ||
| 86 | - ImGroupMemberDO after2 = mapper.selectById(u2.getId()); | ||
| 87 | - assertEquals(CommonStatusEnum.DISABLE.getStatus(), after2.getStatus()); | ||
| 88 | - assertTrue(after2.getQuitTime().isBefore(now.minusDays(1))); | ||
| 89 | - } | ||
| 90 | - | ||
| 91 | - // ========== updateByGroupIdAndStatus ========== | ||
| 92 | - | ||
| 93 | - @Test | ||
| 94 | - public void testUpdateByGroupIdAndStatus_allActiveInGroup() { | ||
| 95 | - // 准备:群 10 有 2 个活跃 + 1 个已退;群 20 有 1 个活跃 | ||
| 96 | - ImGroupMemberDO a1 = buildMember(10L, 1L, CommonStatusEnum.ENABLE); | ||
| 97 | - mapper.insert(a1); | ||
| 98 | - ImGroupMemberDO a2 = buildMember(10L, 2L, CommonStatusEnum.ENABLE); | ||
| 99 | - mapper.insert(a2); | ||
| 100 | - ImGroupMemberDO a3 = buildMember(10L, 3L, CommonStatusEnum.DISABLE); | ||
| 101 | - a3.setQuitTime(LocalDateTime.now().minusDays(5)); | ||
| 102 | - mapper.insert(a3); | ||
| 103 | - ImGroupMemberDO b1 = buildMember(20L, 1L, CommonStatusEnum.ENABLE); | ||
| 104 | - mapper.insert(b1); | ||
| 105 | - | ||
| 106 | - // 调用:解散群 10 | ||
| 107 | - mapper.updateByGroupIdAndStatus(10L, CommonStatusEnum.ENABLE.getStatus(), | ||
| 108 | - new ImGroupMemberDO().setStatus(CommonStatusEnum.DISABLE.getStatus()) | ||
| 109 | - .setQuitTime(LocalDateTime.now())); | ||
| 110 | - | ||
| 111 | - // 断言:群 10 的活跃成员全部变为 DISABLE,群 20 的不受影响 | ||
| 112 | - assertEquals(CommonStatusEnum.DISABLE.getStatus(), mapper.selectById(a1.getId()).getStatus()); | ||
| 113 | - assertEquals(CommonStatusEnum.DISABLE.getStatus(), mapper.selectById(a2.getId()).getStatus()); | ||
| 114 | - assertEquals(CommonStatusEnum.ENABLE.getStatus(), mapper.selectById(b1.getId()).getStatus()); | ||
| 115 | - } | ||
| 116 | - | ||
| 117 | - // ========== 工具方法 ========== | ||
| 118 | - | ||
| 119 | - private ImGroupMemberDO buildMember(Long groupId, Long userId, CommonStatusEnum status) { | ||
| 120 | - return ImGroupMemberDO.builder() | ||
| 121 | - .groupId(groupId).userId(userId) | ||
| 122 | - .status(status.getStatus()) | ||
| 123 | - .joinTime(LocalDateTime.now().minusDays(30)) | ||
| 124 | - .build(); | ||
| 125 | - } | ||
| 126 | - | ||
| 127 | -} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/message/ImGroupMessageMapperTest.java
| @@ -43,8 +43,8 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { | @@ -43,8 +43,8 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { | ||
| 43 | } | 43 | } |
| 44 | 44 | ||
| 45 | @Test | 45 | @Test |
| 46 | - public void testSelectListByMinId_excludeRecall() { | ||
| 47 | - // 准备:一条正常 + 一条撤回 | 46 | + public void testSelectListByMinId_includesRecall() { |
| 47 | + // 准备:一条正常 + 一条撤回;客户端拉离线消息需要拿到撤回状态以渲染「此消息已撤回」占位 | ||
| 48 | ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | 48 | ImGroupMessageDO normal = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); |
| 49 | mapper.insert(normal); | 49 | mapper.insert(normal); |
| 50 | ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); | 50 | ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); |
| @@ -52,8 +52,8 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { | @@ -52,8 +52,8 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { | ||
| 52 | 52 | ||
| 53 | List<ImGroupMessageDO> result = mapper.selectListByMinId(List.of(10L), 0L, FAR_PAST, 100); | 53 | List<ImGroupMessageDO> result = mapper.selectListByMinId(List.of(10L), 0L, FAR_PAST, 100); |
| 54 | 54 | ||
| 55 | - assertEquals(1, result.size()); | ||
| 56 | - assertEquals(normal.getId(), result.get(0).getId()); | 55 | + // 断言:撤回消息一并返回,由客户端按 status 切换渲染 |
| 56 | + assertEquals(2, result.size()); | ||
| 57 | } | 57 | } |
| 58 | 58 | ||
| 59 | @Test | 59 | @Test |
| @@ -211,64 +211,6 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { | @@ -211,64 +211,6 @@ public class ImGroupMessageMapperTest extends BaseDbUnitTest { | ||
| 211 | assertEquals(after.getId(), result.get(0).getId()); | 211 | assertEquals(after.getId(), result.get(0).getId()); |
| 212 | } | 212 | } |
| 213 | 213 | ||
| 214 | - // ========== selectListByGroupIdAndPendingReceipt ========== | ||
| 215 | - | ||
| 216 | - @Test | ||
| 217 | - public void testSelectListByGroupIdAndPendingReceipt_basic() { | ||
| 218 | - // 准备:待回执 + 已完成 + 无需回执,三种状态 | ||
| 219 | - ImGroupMessageDO pending = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 220 | - pending.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 221 | - mapper.insert(pending); | ||
| 222 | - ImGroupMessageDO done = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 223 | - done.setReceiptStatus(ImGroupMessageReceiptStatusEnum.DONE.getStatus()); | ||
| 224 | - mapper.insert(done); | ||
| 225 | - ImGroupMessageDO noReceipt = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 226 | - noReceipt.setReceiptStatus(ImGroupMessageReceiptStatusEnum.NO_RECEIPT.getStatus()); | ||
| 227 | - mapper.insert(noReceipt); | ||
| 228 | - | ||
| 229 | - List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndPendingReceipt(10L, null, Long.MAX_VALUE); | ||
| 230 | - | ||
| 231 | - // 断言:只返回 pending 记录 | ||
| 232 | - assertEquals(1, result.size()); | ||
| 233 | - assertEquals(pending.getId(), result.get(0).getId()); | ||
| 234 | - } | ||
| 235 | - | ||
| 236 | - @Test | ||
| 237 | - public void testSelectListByGroupIdAndPendingReceipt_windowAndRecall() { | ||
| 238 | - // 准备:窗口外 + 撤回 + 窗口内 pending | ||
| 239 | - ImGroupMessageDO belowRange = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 240 | - belowRange.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 241 | - mapper.insert(belowRange); | ||
| 242 | - ImGroupMessageDO inRange = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 243 | - inRange.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 244 | - mapper.insert(inRange); | ||
| 245 | - ImGroupMessageDO recalled = buildMessage(10L, 1L, ImMessageStatusEnum.RECALL); | ||
| 246 | - recalled.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 247 | - mapper.insert(recalled); | ||
| 248 | - | ||
| 249 | - // 调用:minId = belowRange.id(排除), maxId = inRange.id(包含) | ||
| 250 | - List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndPendingReceipt( | ||
| 251 | - 10L, belowRange.getId(), inRange.getId()); | ||
| 252 | - | ||
| 253 | - // 断言:只返回窗口内的非撤回 pending,即 inRange | ||
| 254 | - assertEquals(1, result.size()); | ||
| 255 | - assertEquals(inRange.getId(), result.get(0).getId()); | ||
| 256 | - } | ||
| 257 | - | ||
| 258 | - @Test | ||
| 259 | - public void testSelectListByGroupIdAndPendingReceipt_otherGroupExcluded() { | ||
| 260 | - ImGroupMessageDO g10 = buildMessage(10L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 261 | - g10.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 262 | - mapper.insert(g10); | ||
| 263 | - ImGroupMessageDO g20 = buildMessage(20L, 1L, ImMessageStatusEnum.UNREAD); | ||
| 264 | - g20.setReceiptStatus(ImGroupMessageReceiptStatusEnum.PENDING.getStatus()); | ||
| 265 | - mapper.insert(g20); | ||
| 266 | - | ||
| 267 | - List<ImGroupMessageDO> result = mapper.selectListByGroupIdAndPendingReceipt(10L, null, Long.MAX_VALUE); | ||
| 268 | - assertEquals(1, result.size()); | ||
| 269 | - assertEquals(g10.getId(), result.get(0).getId()); | ||
| 270 | - } | ||
| 271 | - | ||
| 272 | // ========== 工具方法 ========== | 214 | // ========== 工具方法 ========== |
| 273 | 215 | ||
| 274 | private ImGroupMessageDO buildMessage(Long groupId, Long senderId, ImMessageStatusEnum status) { | 216 | private ImGroupMessageDO buildMessage(Long groupId, Long senderId, ImMessageStatusEnum status) { |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/rtc/ImRtcParticipantMapperTest.java deleted
| 1 | -package cn.iocoder.yudao.module.im.dal.mysql.rtc; | ||
| 2 | - | ||
| 3 | -import cn.iocoder.yudao.framework.test.core.ut.BaseDbUnitTest; | ||
| 4 | -import cn.iocoder.yudao.module.im.dal.dataobject.rtc.ImRtcParticipantDO; | ||
| 5 | -import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantRoleEnum; | ||
| 6 | -import cn.iocoder.yudao.module.im.enums.rtc.ImRtcParticipantStatusEnum; | ||
| 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 | -public class ImRtcParticipantMapperTest extends BaseDbUnitTest { | ||
| 16 | - | ||
| 17 | - @Resource | ||
| 18 | - private ImRtcParticipantMapper mapper; | ||
| 19 | - | ||
| 20 | - // ========== selectByRoomAndUserId ========== | ||
| 21 | - | ||
| 22 | - @Test | ||
| 23 | - public void testSelectByRoomAndUserId_match() { | ||
| 24 | - // 准备:room=r1 / userId=100 命中;同 room 其他 user 不应误命中 | ||
| 25 | - ImRtcParticipantDO target = insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING); | ||
| 26 | - insert("r1", 101L, ImRtcParticipantStatusEnum.INVITING); | ||
| 27 | - insert("r2", 100L, ImRtcParticipantStatusEnum.INVITING); | ||
| 28 | - | ||
| 29 | - // 调用 + 断言 | ||
| 30 | - ImRtcParticipantDO got = mapper.selectByRoomAndUserId("r1", 100L); | ||
| 31 | - assertNotNull(got); | ||
| 32 | - assertEquals(target.getId(), got.getId()); | ||
| 33 | - } | ||
| 34 | - | ||
| 35 | - @Test | ||
| 36 | - public void testSelectByRoomAndUserId_miss() { | ||
| 37 | - // 准备:room 不存在的查询返 null | ||
| 38 | - insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING); | ||
| 39 | - | ||
| 40 | - // 调用 + 断言 | ||
| 41 | - assertNull(mapper.selectByRoomAndUserId("r2", 100L)); | ||
| 42 | - assertNull(mapper.selectByRoomAndUserId("r1", 999L)); | ||
| 43 | - } | ||
| 44 | - | ||
| 45 | - // ========== selectListByRoom ========== | ||
| 46 | - | ||
| 47 | - @Test | ||
| 48 | - public void testSelectListByRoom_filterByRoom() { | ||
| 49 | - // 准备:r1 两条、r2 一条 | ||
| 50 | - insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING); | ||
| 51 | - insert("r1", 101L, ImRtcParticipantStatusEnum.JOINED); | ||
| 52 | - insert("r2", 200L, ImRtcParticipantStatusEnum.INVITING); | ||
| 53 | - | ||
| 54 | - // 调用 + 断言:仅 r1 命中两条 | ||
| 55 | - List<ImRtcParticipantDO> result = mapper.selectListByRoom("r1"); | ||
| 56 | - assertEquals(2, result.size()); | ||
| 57 | - assertTrue(result.stream().allMatch(p -> "r1".equals(p.getRoom()))); | ||
| 58 | - } | ||
| 59 | - | ||
| 60 | - // ========== selectListByStatusAndInviteTimeBefore ========== | ||
| 61 | - | ||
| 62 | - @Test | ||
| 63 | - public void testSelectListByStatusAndInviteTimeBefore_filterStatusAndTime() { | ||
| 64 | - // 准备:3 条 INVITING(一旧两新)+ 1 条 JOINED(旧,状态不匹配) | ||
| 65 | - LocalDateTime now = LocalDateTime.now(); | ||
| 66 | - ImRtcParticipantDO oldInviting = insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING, now.minusMinutes(5)); | ||
| 67 | - insert("r1", 101L, ImRtcParticipantStatusEnum.INVITING, now.minusSeconds(10)); | ||
| 68 | - insert("r2", 102L, ImRtcParticipantStatusEnum.INVITING, now.minusSeconds(10)); | ||
| 69 | - insert("r3", 103L, ImRtcParticipantStatusEnum.JOINED, now.minusMinutes(5)); | ||
| 70 | - | ||
| 71 | - // 调用:cutoff = now - 1 分钟,只命中早于该时间的 INVITING | ||
| 72 | - List<ImRtcParticipantDO> result = mapper.selectListByStatusAndInviteTimeBefore( | ||
| 73 | - ImRtcParticipantStatusEnum.INVITING.getStatus(), now.minusMinutes(1)); | ||
| 74 | - | ||
| 75 | - // 断言:仅 oldInviting 命中(status + time 双重过滤) | ||
| 76 | - assertEquals(1, result.size()); | ||
| 77 | - assertEquals(oldInviting.getId(), result.get(0).getId()); | ||
| 78 | - } | ||
| 79 | - | ||
| 80 | - @Test | ||
| 81 | - public void testSelectListByStatusAndInviteTimeBefore_emptyWhenNoMatch() { | ||
| 82 | - // 准备:所有 INVITING 都是新鲜的 | ||
| 83 | - LocalDateTime now = LocalDateTime.now(); | ||
| 84 | - insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING, now.minusSeconds(10)); | ||
| 85 | - | ||
| 86 | - // 调用 + 断言:cutoff 在更早的时间,无命中 | ||
| 87 | - List<ImRtcParticipantDO> result = mapper.selectListByStatusAndInviteTimeBefore( | ||
| 88 | - ImRtcParticipantStatusEnum.INVITING.getStatus(), now.minusMinutes(5)); | ||
| 89 | - assertTrue(result.isEmpty()); | ||
| 90 | - } | ||
| 91 | - | ||
| 92 | - // ========== selectListByRoomAndStatusAndInviteTimeBefore ========== | ||
| 93 | - | ||
| 94 | - @Test | ||
| 95 | - public void testSelectListByRoomAndStatusAndInviteTimeBefore_limitToRoom() { | ||
| 96 | - // 准备:r1 一旧 INVITING;r2 一旧 INVITING;r1 一新 INVITING | ||
| 97 | - LocalDateTime now = LocalDateTime.now(); | ||
| 98 | - ImRtcParticipantDO r1Old = insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING, now.minusMinutes(5)); | ||
| 99 | - insert("r2", 101L, ImRtcParticipantStatusEnum.INVITING, now.minusMinutes(5)); | ||
| 100 | - insert("r1", 102L, ImRtcParticipantStatusEnum.INVITING, now.minusSeconds(10)); | ||
| 101 | - | ||
| 102 | - // 调用:限定 room=r1,cutoff = now - 1 分钟 | ||
| 103 | - List<ImRtcParticipantDO> result = mapper.selectListByRoomAndStatusAndInviteTimeBefore( | ||
| 104 | - "r1", ImRtcParticipantStatusEnum.INVITING.getStatus(), now.minusMinutes(1)); | ||
| 105 | - | ||
| 106 | - // 断言:r2 的旧 INVITING 被 room 过滤掉;r1 的新 INVITING 被 time 过滤掉 | ||
| 107 | - assertEquals(1, result.size()); | ||
| 108 | - assertEquals(r1Old.getId(), result.get(0).getId()); | ||
| 109 | - } | ||
| 110 | - | ||
| 111 | - @Test | ||
| 112 | - public void testSelectListByRoomAndStatusAndInviteTimeBefore_ignoresOtherStatus() { | ||
| 113 | - // 准备:r1 一旧 JOINED(虽然 time 满足,status 不匹配)+ 一旧 REJECTED | ||
| 114 | - LocalDateTime now = LocalDateTime.now(); | ||
| 115 | - insert("r1", 100L, ImRtcParticipantStatusEnum.JOINED, now.minusMinutes(5)); | ||
| 116 | - insert("r1", 101L, ImRtcParticipantStatusEnum.REJECTED, now.minusMinutes(5)); | ||
| 117 | - | ||
| 118 | - // 调用 + 断言:仅扫 INVITING 状态,无命中 | ||
| 119 | - List<ImRtcParticipantDO> result = mapper.selectListByRoomAndStatusAndInviteTimeBefore( | ||
| 120 | - "r1", ImRtcParticipantStatusEnum.INVITING.getStatus(), now.minusMinutes(1)); | ||
| 121 | - assertTrue(result.isEmpty()); | ||
| 122 | - } | ||
| 123 | - | ||
| 124 | - // ========== selectLastOneByUserIdAndStatus ========== | ||
| 125 | - | ||
| 126 | - @Test | ||
| 127 | - public void testSelectLastOneByUserIdAndStatus_returnsLatestActive() { | ||
| 128 | - // 准备:user 100 两条 INVITING(不同 room)+ 一条 LEFT | ||
| 129 | - insert("r1", 100L, ImRtcParticipantStatusEnum.LEFT); | ||
| 130 | - insert("r2", 100L, ImRtcParticipantStatusEnum.INVITING); | ||
| 131 | - ImRtcParticipantDO latest = insert("r3", 100L, ImRtcParticipantStatusEnum.JOINED); | ||
| 132 | - | ||
| 133 | - // 调用:忙线检测取 ACTIVE_STATUSES,应取 id 最大那条 | ||
| 134 | - ImRtcParticipantDO got = mapper.selectLastOneByUserIdAndStatus(100L, | ||
| 135 | - ImRtcParticipantStatusEnum.ACTIVE_STATUSES); | ||
| 136 | - | ||
| 137 | - // 断言:返回最新的 JOINED 记录(id 最大) | ||
| 138 | - assertNotNull(got); | ||
| 139 | - assertEquals(latest.getId(), got.getId()); | ||
| 140 | - } | ||
| 141 | - | ||
| 142 | - @Test | ||
| 143 | - public void testSelectLastOneByUserIdAndStatus_missWhenAllTerminal() { | ||
| 144 | - // 准备:user 100 只有终态记录 | ||
| 145 | - insert("r1", 100L, ImRtcParticipantStatusEnum.LEFT); | ||
| 146 | - insert("r2", 100L, ImRtcParticipantStatusEnum.REJECTED); | ||
| 147 | - | ||
| 148 | - // 调用 + 断言:忙线检测无命中 | ||
| 149 | - assertNull(mapper.selectLastOneByUserIdAndStatus(100L, | ||
| 150 | - ImRtcParticipantStatusEnum.ACTIVE_STATUSES)); | ||
| 151 | - } | ||
| 152 | - | ||
| 153 | - // ========== updateByIdAndStatus(振铃超时 CAS 路径关键) ========== | ||
| 154 | - | ||
| 155 | - @Test | ||
| 156 | - public void testUpdateByIdAndStatus_inviting2NoAnswer_success() { | ||
| 157 | - // 准备:一条 INVITING 候选 | ||
| 158 | - ImRtcParticipantDO p = insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING, LocalDateTime.now().minusMinutes(5)); | ||
| 159 | - | ||
| 160 | - // 调用:CAS INVITING → NO_ANSWER;模拟 Job 单参与者抢占 | ||
| 161 | - int updated = mapper.updateByIdAndStatus(p.getId(), | ||
| 162 | - ImRtcParticipantStatusEnum.INVITING.getStatus(), | ||
| 163 | - new ImRtcParticipantDO().setId(p.getId()) | ||
| 164 | - .setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus()) | ||
| 165 | - .setLeaveTime(LocalDateTime.now())); | ||
| 166 | - | ||
| 167 | - // 断言:CAS 成功 + 状态真的落地 | ||
| 168 | - assertEquals(1, updated); | ||
| 169 | - assertEquals(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus(), | ||
| 170 | - mapper.selectById(p.getId()).getStatus()); | ||
| 171 | - } | ||
| 172 | - | ||
| 173 | - @Test | ||
| 174 | - public void testUpdateByIdAndStatus_concurrentReject_casFails() { | ||
| 175 | - // 准备:一条记录已经被用户主动 reject(状态变成 REJECTED) | ||
| 176 | - ImRtcParticipantDO p = insert("r1", 100L, ImRtcParticipantStatusEnum.REJECTED, LocalDateTime.now().minusMinutes(5)); | ||
| 177 | - | ||
| 178 | - // 调用:Job 仍按 INVITING 抢占;并发已变更应失败 | ||
| 179 | - int updated = mapper.updateByIdAndStatus(p.getId(), | ||
| 180 | - ImRtcParticipantStatusEnum.INVITING.getStatus(), | ||
| 181 | - new ImRtcParticipantDO().setId(p.getId()) | ||
| 182 | - .setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus()) | ||
| 183 | - .setLeaveTime(LocalDateTime.now())); | ||
| 184 | - | ||
| 185 | - // 断言:CAS 失败 + 原状态保留 | ||
| 186 | - assertEquals(0, updated); | ||
| 187 | - assertEquals(ImRtcParticipantStatusEnum.REJECTED.getStatus(), | ||
| 188 | - mapper.selectById(p.getId()).getStatus()); | ||
| 189 | - } | ||
| 190 | - | ||
| 191 | - // ========== updateByRoomAndStatus(endSession 批量改 INVITING → NO_ANSWER 路径关键) ========== | ||
| 192 | - | ||
| 193 | - @Test | ||
| 194 | - public void testUpdateByRoomAndStatus_inviting2NoAnswer_batchUpdate() { | ||
| 195 | - // 准备:r1 两条 INVITING + 一条 JOINED(状态不匹配,不应被改);r2 一条 INVITING(room 不匹配,不应被改) | ||
| 196 | - ImRtcParticipantDO inv1 = insert("r1", 100L, ImRtcParticipantStatusEnum.INVITING); | ||
| 197 | - ImRtcParticipantDO inv2 = insert("r1", 101L, ImRtcParticipantStatusEnum.INVITING); | ||
| 198 | - ImRtcParticipantDO joined = insert("r1", 102L, ImRtcParticipantStatusEnum.JOINED); | ||
| 199 | - ImRtcParticipantDO otherRoom = insert("r2", 103L, ImRtcParticipantStatusEnum.INVITING); | ||
| 200 | - | ||
| 201 | - // 调用:模拟 endSession 把残留 INVITING 批量改 NO_ANSWER | ||
| 202 | - int updated = mapper.updateByRoomAndStatus("r1", | ||
| 203 | - ImRtcParticipantStatusEnum.INVITING.getStatus(), | ||
| 204 | - new ImRtcParticipantDO().setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus())); | ||
| 205 | - | ||
| 206 | - // 断言:仅 r1 的 2 条 INVITING 被改,其它保持 | ||
| 207 | - assertEquals(2, updated); | ||
| 208 | - assertEquals(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus(), mapper.selectById(inv1.getId()).getStatus()); | ||
| 209 | - assertEquals(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus(), mapper.selectById(inv2.getId()).getStatus()); | ||
| 210 | - assertEquals(ImRtcParticipantStatusEnum.JOINED.getStatus(), mapper.selectById(joined.getId()).getStatus()); | ||
| 211 | - assertEquals(ImRtcParticipantStatusEnum.INVITING.getStatus(), mapper.selectById(otherRoom.getId()).getStatus()); | ||
| 212 | - } | ||
| 213 | - | ||
| 214 | - @Test | ||
| 215 | - public void testUpdateByRoomAndStatus_noMatch_returnsZero() { | ||
| 216 | - // 准备:r1 只有 JOINED 状态 | ||
| 217 | - insert("r1", 100L, ImRtcParticipantStatusEnum.JOINED); | ||
| 218 | - | ||
| 219 | - // 调用:找 INVITING 应无命中 | ||
| 220 | - int updated = mapper.updateByRoomAndStatus("r1", | ||
| 221 | - ImRtcParticipantStatusEnum.INVITING.getStatus(), | ||
| 222 | - new ImRtcParticipantDO().setStatus(ImRtcParticipantStatusEnum.NO_ANSWER.getStatus())); | ||
| 223 | - | ||
| 224 | - // 断言:0 行受影响 | ||
| 225 | - assertEquals(0, updated); | ||
| 226 | - } | ||
| 227 | - | ||
| 228 | - private ImRtcParticipantDO insert(String room, Long userId, ImRtcParticipantStatusEnum status) { | ||
| 229 | - return insert(room, userId, status, LocalDateTime.now()); | ||
| 230 | - } | ||
| 231 | - | ||
| 232 | - private ImRtcParticipantDO insert(String room, Long userId, ImRtcParticipantStatusEnum status, LocalDateTime inviteTime) { | ||
| 233 | - ImRtcParticipantDO p = new ImRtcParticipantDO() | ||
| 234 | - .setRoom(room) | ||
| 235 | - .setUserId(userId) | ||
| 236 | - .setRole(ImRtcParticipantRoleEnum.INVITEE.getRole()) | ||
| 237 | - .setStatus(status.getStatus()) | ||
| 238 | - .setInviteTime(inviteTime); | ||
| 239 | - mapper.insert(p); | ||
| 240 | - return p; | ||
| 241 | - } | ||
| 242 | - | ||
| 243 | -} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/dal/mysql/statistics/ImStatisticsManagerMapperTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.dal.mysql.statistics; | ||
| 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.ImGroupDO; | ||
| 6 | +import cn.iocoder.yudao.module.im.dal.dataobject.group.ImGroupMemberDO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImGroupMessageDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.message.ImPrivateMessageDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; | ||
| 10 | +import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMemberMapper; | ||
| 11 | +import cn.iocoder.yudao.module.im.dal.mysql.message.ImGroupMessageMapper; | ||
| 12 | +import cn.iocoder.yudao.module.im.dal.mysql.message.ImPrivateMessageMapper; | ||
| 13 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageStatusEnum; | ||
| 14 | +import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | ||
| 15 | +import jakarta.annotation.Resource; | ||
| 16 | +import org.junit.jupiter.api.BeforeEach; | ||
| 17 | +import org.junit.jupiter.api.Test; | ||
| 18 | +import org.springframework.jdbc.core.JdbcTemplate; | ||
| 19 | + | ||
| 20 | +import javax.sql.DataSource; | ||
| 21 | +import java.time.LocalDateTime; | ||
| 22 | +import java.util.List; | ||
| 23 | +import java.util.Map; | ||
| 24 | + | ||
| 25 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * IM 数据看板 Mapper 单元测试 | ||
| 29 | + * | ||
| 30 | + * @author 芋道源码 | ||
| 31 | + */ | ||
| 32 | +public class ImStatisticsManagerMapperTest extends BaseDbUnitTest { | ||
| 33 | + | ||
| 34 | + private static final LocalDateTime WINDOW_BEGIN = LocalDateTime.of(2026, 1, 1, 0, 0); | ||
| 35 | + private static final LocalDateTime WINDOW_END = LocalDateTime.of(2026, 2, 1, 0, 0); | ||
| 36 | + | ||
| 37 | + @Resource | ||
| 38 | + private ImStatisticsManagerMapper mapper; | ||
| 39 | + | ||
| 40 | + @Resource | ||
| 41 | + private ImPrivateMessageMapper privateMessageMapper; | ||
| 42 | + @Resource | ||
| 43 | + private ImGroupMessageMapper groupMessageMapper; | ||
| 44 | + @Resource | ||
| 45 | + private ImGroupMapper groupMapper; | ||
| 46 | + @Resource | ||
| 47 | + private ImGroupMemberMapper groupMemberMapper; | ||
| 48 | + @Resource | ||
| 49 | + private DataSource dataSource; | ||
| 50 | + | ||
| 51 | + private JdbcTemplate jdbcTemplate; | ||
| 52 | + | ||
| 53 | + @BeforeEach | ||
| 54 | + public void initJdbcTemplate() { | ||
| 55 | + jdbcTemplate = new JdbcTemplate(dataSource); | ||
| 56 | + } | ||
| 57 | + | ||
| 58 | + // ========== 用户 ========== | ||
| 59 | + | ||
| 60 | + @Test | ||
| 61 | + public void testSelectTotalUserCount() { | ||
| 62 | + // 准备:3 个未删除用户 | ||
| 63 | + insertUser(1L, LocalDateTime.now()); | ||
| 64 | + insertUser(2L, LocalDateTime.now()); | ||
| 65 | + insertUser(3L, LocalDateTime.now()); | ||
| 66 | + | ||
| 67 | + // 调用 + 断言 | ||
| 68 | + assertEquals(3L, mapper.selectTotalUserCount()); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + @Test | ||
| 72 | + public void testSelectNewUserCount_windowFilter() { | ||
| 73 | + // 准备:1 个在窗口内 + 2 个在窗口外 | ||
| 74 | + insertUser(1L, WINDOW_BEGIN.plusDays(3)); | ||
| 75 | + insertUser(2L, WINDOW_BEGIN.minusDays(1)); | ||
| 76 | + insertUser(3L, WINDOW_END.plusDays(1)); | ||
| 77 | + | ||
| 78 | + // 调用 + 断言:只命中窗口内 | ||
| 79 | + assertEquals(1L, mapper.selectNewUserCount(WINDOW_BEGIN, WINDOW_END)); | ||
| 80 | + } | ||
| 81 | + | ||
| 82 | + @Test | ||
| 83 | + public void testSelectActiveUserCount_distinctAcrossPrivateAndGroup() { | ||
| 84 | + // 准备:1 私聊;2 群聊;3 在窗口外;1 既发私聊又发群聊(去重) | ||
| 85 | + privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.plusDays(1))); | ||
| 86 | + privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.plusDays(2))); | ||
| 87 | + groupMessageMapper.insert(buildGroupMessage(2L, 100L, WINDOW_BEGIN.plusDays(3))); | ||
| 88 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(4))); // 用户 1 已计一次 | ||
| 89 | + privateMessageMapper.insert(buildPrivate(3L, 4L, WINDOW_BEGIN.minusDays(1))); // 窗口外 | ||
| 90 | + | ||
| 91 | + // 调用 + 断言:去重后 = 2 | ||
| 92 | + assertEquals(2L, mapper.selectActiveUserCount(WINDOW_BEGIN, WINDOW_END)); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + @Test | ||
| 96 | + public void testSelectNewUserDailyCount_groupByDay() { | ||
| 97 | + // 准备:第 1 天 2 个;第 2 天 1 个;窗口外 1 个 | ||
| 98 | + insertUser(1L, WINDOW_BEGIN.plusDays(1)); | ||
| 99 | + insertUser(2L, WINDOW_BEGIN.plusDays(1)); | ||
| 100 | + insertUser(3L, WINDOW_BEGIN.plusDays(2)); | ||
| 101 | + insertUser(4L, WINDOW_BEGIN.minusDays(1)); | ||
| 102 | + | ||
| 103 | + // 调用 | ||
| 104 | + List<Map<String, Object>> list = mapper.selectNewUserDailyCount(WINDOW_BEGIN, WINDOW_END); | ||
| 105 | + | ||
| 106 | + // 断言:返回两个分组、总数 = 3 | ||
| 107 | + assertEquals(2, list.size()); | ||
| 108 | + long sum = list.stream().mapToLong(m -> ((Number) m.get("count")).longValue()).sum(); | ||
| 109 | + assertEquals(3L, sum); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + @Test | ||
| 113 | + public void testSelectActiveUserDailyCount_distinctPerDay() { | ||
| 114 | + // 准备:同一天里用户 1 在私聊与群聊都发了;第二天用户 2 发了 | ||
| 115 | + privateMessageMapper.insert(buildPrivate(1L, 9L, WINDOW_BEGIN.plusDays(1))); | ||
| 116 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1))); | ||
| 117 | + groupMessageMapper.insert(buildGroupMessage(2L, 100L, WINDOW_BEGIN.plusDays(2))); | ||
| 118 | + | ||
| 119 | + // 调用 | ||
| 120 | + List<Map<String, Object>> list = mapper.selectActiveUserDailyCount(WINDOW_BEGIN, WINDOW_END); | ||
| 121 | + | ||
| 122 | + // 断言:两天合计 2 名活跃用户(同一天 user 1 去重 + 第二天 user 2) | ||
| 123 | + assertEquals(2, list.size()); | ||
| 124 | + long sum = list.stream().mapToLong(m -> ((Number) m.get("count")).longValue()).sum(); | ||
| 125 | + assertEquals(2L, sum); | ||
| 126 | + } | ||
| 127 | + | ||
| 128 | + // ========== 群 ========== | ||
| 129 | + | ||
| 130 | + @Test | ||
| 131 | + public void testSelectTotalGroupCount_onlyEnabled() { | ||
| 132 | + // 准备:2 个正常 + 1 个已解散 | ||
| 133 | + groupMapper.insert(buildGroup(CommonStatusEnum.ENABLE)); | ||
| 134 | + groupMapper.insert(buildGroup(CommonStatusEnum.ENABLE)); | ||
| 135 | + groupMapper.insert(buildGroup(CommonStatusEnum.DISABLE)); | ||
| 136 | + | ||
| 137 | + // 调用 + 断言 | ||
| 138 | + assertEquals(2L, mapper.selectTotalGroupCount()); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + @Test | ||
| 142 | + public void testSelectNewGroupCount_windowFilter() { | ||
| 143 | + // 准备:1 个窗口内 + 1 个窗口外 | ||
| 144 | + groupMapper.insert(buildGroup(CommonStatusEnum.ENABLE)); | ||
| 145 | + groupMapper.insert(buildGroup(CommonStatusEnum.ENABLE)); | ||
| 146 | + // 窗口窄到只覆盖 1 秒 | ||
| 147 | + LocalDateTime narrowBegin = LocalDateTime.now().plusYears(10); | ||
| 148 | + LocalDateTime narrowEnd = narrowBegin.plusSeconds(1); | ||
| 149 | + | ||
| 150 | + // 调用 + 断言:极窄窗口不命中任何群 | ||
| 151 | + assertEquals(0L, mapper.selectNewGroupCount(narrowBegin, narrowEnd)); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + @Test | ||
| 155 | + public void testSelectGroupSizeDistribution_bucketing() { | ||
| 156 | + // 准备:一个 5 人群、一个 15 人群 | ||
| 157 | + Long groupSmall = insertGroupAndMembers(5); | ||
| 158 | + Long groupMid = insertGroupAndMembers(15); | ||
| 159 | + assertNotNull(groupSmall); | ||
| 160 | + assertNotNull(groupMid); | ||
| 161 | + | ||
| 162 | + // 调用 | ||
| 163 | + List<Map<String, Object>> dist = mapper.selectGroupSizeDistribution(); | ||
| 164 | + | ||
| 165 | + // 断言:分桶包含「1-9 人」和「10-49 人」 | ||
| 166 | + Map<Object, Long> byRange = dist.stream().collect(java.util.stream.Collectors.toMap( | ||
| 167 | + m -> m.get("range"), m -> ((Number) m.get("count")).longValue())); | ||
| 168 | + assertEquals(1L, byRange.get("1-9 人")); | ||
| 169 | + assertEquals(1L, byRange.get("10-49 人")); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + // ========== 消息 ========== | ||
| 173 | + | ||
| 174 | + @Test | ||
| 175 | + public void testSelectPrivateMessageCount_windowFilter() { | ||
| 176 | + // 准备:1 在窗口内 + 1 在窗口外 | ||
| 177 | + privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.plusDays(1))); | ||
| 178 | + privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.minusDays(1))); | ||
| 179 | + | ||
| 180 | + // 调用 + 断言 | ||
| 181 | + assertEquals(1L, mapper.selectPrivateMessageCount(WINDOW_BEGIN, WINDOW_END)); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | + @Test | ||
| 185 | + public void testSelectGroupMessageCount_windowFilter() { | ||
| 186 | + // 准备:1 在窗口内 + 1 在窗口外 | ||
| 187 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1))); | ||
| 188 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.minusDays(1))); | ||
| 189 | + | ||
| 190 | + // 调用 + 断言 | ||
| 191 | + assertEquals(1L, mapper.selectGroupMessageCount(WINDOW_BEGIN, WINDOW_END)); | ||
| 192 | + } | ||
| 193 | + | ||
| 194 | + @Test | ||
| 195 | + public void testSelectMessageTypeDistribution_mergePrivateAndGroup() { | ||
| 196 | + // 准备:私聊 type=0 ×1,群聊 type=0 ×2、type=1 ×1 | ||
| 197 | + privateMessageMapper.insert(buildPrivate(1L, 2L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.TEXT.getType())); | ||
| 198 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.TEXT.getType())); | ||
| 199 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.TEXT.getType())); | ||
| 200 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1)).setType(ImMessageTypeEnum.IMAGE.getType())); | ||
| 201 | + | ||
| 202 | + // 调用 | ||
| 203 | + List<Map<String, Object>> dist = mapper.selectMessageTypeDistribution(WINDOW_BEGIN, WINDOW_END); | ||
| 204 | + | ||
| 205 | + // 断言:type=TEXT(0) → 3;type=IMAGE(1) → 1 | ||
| 206 | + Map<Object, Long> byType = dist.stream().collect(java.util.stream.Collectors.toMap( | ||
| 207 | + m -> ((Number) m.get("type")).intValue(), m -> ((Number) m.get("count")).longValue())); | ||
| 208 | + assertEquals(3L, byType.get(ImMessageTypeEnum.TEXT.getType())); | ||
| 209 | + assertEquals(1L, byType.get(ImMessageTypeEnum.IMAGE.getType())); | ||
| 210 | + } | ||
| 211 | + | ||
| 212 | + @Test | ||
| 213 | + public void testSelectTopSenders_orderByCountDescAndLimit() { | ||
| 214 | + // 准备:user 1 共 3 条;user 2 共 2 条;user 3 共 1 条 | ||
| 215 | + privateMessageMapper.insert(buildPrivate(1L, 9L, WINDOW_BEGIN.plusDays(1))); | ||
| 216 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1))); | ||
| 217 | + groupMessageMapper.insert(buildGroupMessage(1L, 100L, WINDOW_BEGIN.plusDays(1))); | ||
| 218 | + privateMessageMapper.insert(buildPrivate(2L, 9L, WINDOW_BEGIN.plusDays(1))); | ||
| 219 | + groupMessageMapper.insert(buildGroupMessage(2L, 100L, WINDOW_BEGIN.plusDays(1))); | ||
| 220 | + privateMessageMapper.insert(buildPrivate(3L, 9L, WINDOW_BEGIN.plusDays(1))); | ||
| 221 | + | ||
| 222 | + // 调用:取 TOP 2 | ||
| 223 | + List<Map<String, Object>> tops = mapper.selectTopSenders(WINDOW_BEGIN, WINDOW_END, 2); | ||
| 224 | + | ||
| 225 | + // 断言:返回 2 条,按消息数倒序,user1 > user2 | ||
| 226 | + assertEquals(2, tops.size()); | ||
| 227 | + assertEquals(1L, ((Number) tops.get(0).get("userId")).longValue()); | ||
| 228 | + assertEquals(3L, ((Number) tops.get(0).get("messageCount")).longValue()); | ||
| 229 | + assertEquals(2L, ((Number) tops.get(1).get("userId")).longValue()); | ||
| 230 | + assertEquals(2L, ((Number) tops.get(1).get("messageCount")).longValue()); | ||
| 231 | + } | ||
| 232 | + | ||
| 233 | + // ========== 工具方法 ========== | ||
| 234 | + | ||
| 235 | + private void insertUser(Long id, LocalDateTime createTime) { | ||
| 236 | + jdbcTemplate.update( | ||
| 237 | + "INSERT INTO system_users (id, username, password, nickname, status, create_time, update_time, deleted, tenant_id) " + | ||
| 238 | + "VALUES (?, ?, '', ?, 0, ?, ?, FALSE, 0)", | ||
| 239 | + id, "u" + id, "n" + id, createTime, createTime); | ||
| 240 | + } | ||
| 241 | + | ||
| 242 | + private static ImPrivateMessageDO buildPrivate(Long senderId, Long receiverId, LocalDateTime sendTime) { | ||
| 243 | + return ImPrivateMessageDO.builder() | ||
| 244 | + .clientMessageId("uuid-" + System.nanoTime()) | ||
| 245 | + .senderId(senderId).receiverId(receiverId) | ||
| 246 | + .type(ImMessageTypeEnum.TEXT.getType()) | ||
| 247 | + .content("{}") | ||
| 248 | + .status(ImMessageStatusEnum.UNREAD.getStatus()) | ||
| 249 | + .sendTime(sendTime).build(); | ||
| 250 | + } | ||
| 251 | + | ||
| 252 | + private static ImGroupMessageDO buildGroupMessage(Long senderId, Long groupId, LocalDateTime sendTime) { | ||
| 253 | + return new ImGroupMessageDO() | ||
| 254 | + .setClientMessageId("uuid-" + System.nanoTime()) | ||
| 255 | + .setSenderId(senderId).setGroupId(groupId) | ||
| 256 | + .setType(ImMessageTypeEnum.TEXT.getType()) | ||
| 257 | + .setContent("{}") | ||
| 258 | + .setStatus(ImMessageStatusEnum.UNREAD.getStatus()) | ||
| 259 | + .setSendTime(sendTime); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + private static ImGroupDO buildGroup(CommonStatusEnum status) { | ||
| 263 | + return ImGroupDO.builder().name("g" + System.nanoTime()).ownerUserId(1L) | ||
| 264 | + .status(status.getStatus()).build(); | ||
| 265 | + } | ||
| 266 | + | ||
| 267 | + private Long insertGroupAndMembers(int memberCount) { | ||
| 268 | + ImGroupDO group = buildGroup(CommonStatusEnum.ENABLE); | ||
| 269 | + groupMapper.insert(group); | ||
| 270 | + for (int i = 0; i < memberCount; i++) { | ||
| 271 | + groupMemberMapper.insert(new ImGroupMemberDO() | ||
| 272 | + .setGroupId(group.getId()).setUserId((long) (1000 + i)) | ||
| 273 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()).setRole(3)); | ||
| 274 | + } | ||
| 275 | + return group.getId(); | ||
| 276 | + } | ||
| 277 | + | ||
| 278 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/face/ImFacePackItemServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.face; | ||
| 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.manager.face.vo.item.ImFacePackItemSaveReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackItemDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.dal.mysql.face.ImFacePackItemMapper; | ||
| 10 | +import org.junit.jupiter.api.Test; | ||
| 11 | +import org.mockito.ArgumentCaptor; | ||
| 12 | +import org.mockito.InjectMocks; | ||
| 13 | +import org.mockito.Mock; | ||
| 14 | + | ||
| 15 | +import java.util.Arrays; | ||
| 16 | +import java.util.Collections; | ||
| 17 | +import java.util.List; | ||
| 18 | + | ||
| 19 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_PACK_ITEM_NOT_EXISTS; | ||
| 20 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_PACK_NOT_EXISTS; | ||
| 21 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 22 | +import static org.mockito.ArgumentMatchers.any; | ||
| 23 | +import static org.mockito.ArgumentMatchers.anyLong; | ||
| 24 | +import static org.mockito.Mockito.never; | ||
| 25 | +import static org.mockito.Mockito.verify; | ||
| 26 | +import static org.mockito.Mockito.when; | ||
| 27 | + | ||
| 28 | +/** | ||
| 29 | + * IM 表情包项 Service 单元测试 | ||
| 30 | + * | ||
| 31 | + * @author 芋道源码 | ||
| 32 | + */ | ||
| 33 | +public class ImFacePackItemServiceImplTest extends BaseMockitoUnitTest { | ||
| 34 | + | ||
| 35 | + @InjectMocks | ||
| 36 | + private ImFacePackItemServiceImpl service; | ||
| 37 | + | ||
| 38 | + @Mock | ||
| 39 | + private ImFacePackItemMapper facePackItemMapper; | ||
| 40 | + @Mock | ||
| 41 | + private ImFacePackService facePackService; | ||
| 42 | + | ||
| 43 | + // ========== getEnabledItemListByPackIds ========== | ||
| 44 | + | ||
| 45 | + @Test | ||
| 46 | + public void testGetEnabledItemListByPackIds_emptyShortReturn() { | ||
| 47 | + // 调用 + 断言:空入参直接返回空列表,不查 DB | ||
| 48 | + assertTrue(service.getEnabledItemListByPackIds(Collections.emptyList()).isEmpty()); | ||
| 49 | + verify(facePackItemMapper, never()).selectListByPackIdsAndStatus(any(), any()); | ||
| 50 | + } | ||
| 51 | + | ||
| 52 | + @Test | ||
| 53 | + public void testGetEnabledItemListByPackIds_passesEnableStatus() { | ||
| 54 | + // 准备 | ||
| 55 | + List<Long> packIds = Arrays.asList(1L, 2L); | ||
| 56 | + ImFacePackItemDO item = new ImFacePackItemDO(); | ||
| 57 | + when(facePackItemMapper.selectListByPackIdsAndStatus(packIds, CommonStatusEnum.ENABLE.getStatus())) | ||
| 58 | + .thenReturn(Collections.singletonList(item)); | ||
| 59 | + | ||
| 60 | + // 调用 + 断言 | ||
| 61 | + assertEquals(1, service.getEnabledItemListByPackIds(packIds).size()); | ||
| 62 | + } | ||
| 63 | + | ||
| 64 | + // ========== createFacePackItem ========== | ||
| 65 | + | ||
| 66 | + @Test | ||
| 67 | + public void testCreateFacePackItem_success() { | ||
| 68 | + // 准备 | ||
| 69 | + ImFacePackItemSaveReqVO reqVO = new ImFacePackItemSaveReqVO(); | ||
| 70 | + reqVO.setPackId(10L).setUrl("a.png").setName("dog").setWidth(100).setHeight(100) | ||
| 71 | + .setSort(0).setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 72 | + when(facePackService.validateFacePackExists(10L)).thenReturn(new ImFacePackDO()); | ||
| 73 | + | ||
| 74 | + // 调用 | ||
| 75 | + service.createFacePackItem(reqVO); | ||
| 76 | + | ||
| 77 | + // 断言 | ||
| 78 | + ArgumentCaptor<ImFacePackItemDO> captor = ArgumentCaptor.forClass(ImFacePackItemDO.class); | ||
| 79 | + verify(facePackItemMapper).insert(captor.capture()); | ||
| 80 | + assertEquals(10L, captor.getValue().getPackId()); | ||
| 81 | + assertEquals("a.png", captor.getValue().getUrl()); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + @Test | ||
| 85 | + public void testCreateFacePackItem_packNotExists() { | ||
| 86 | + // 准备:所属表情包不存在 | ||
| 87 | + ImFacePackItemSaveReqVO reqVO = new ImFacePackItemSaveReqVO(); | ||
| 88 | + reqVO.setPackId(99L).setUrl("a.png").setWidth(100).setHeight(100).setSort(0) | ||
| 89 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 90 | + when(facePackService.validateFacePackExists(99L)) | ||
| 91 | + .thenThrow(new ServiceException(FACE_PACK_NOT_EXISTS.getCode(), FACE_PACK_NOT_EXISTS.getMsg())); | ||
| 92 | + | ||
| 93 | + // 调用 + 断言:不落库 | ||
| 94 | + assertThrows(ServiceException.class, () -> service.createFacePackItem(reqVO)); | ||
| 95 | + verify(facePackItemMapper, never()).insert(any(ImFacePackItemDO.class)); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + // ========== updateFacePackItem ========== | ||
| 99 | + | ||
| 100 | + @Test | ||
| 101 | + public void testUpdateFacePackItem_success() { | ||
| 102 | + // 准备 | ||
| 103 | + ImFacePackItemSaveReqVO reqVO = new ImFacePackItemSaveReqVO(); | ||
| 104 | + reqVO.setId(1L).setPackId(10L).setUrl("a.png").setWidth(100).setHeight(100) | ||
| 105 | + .setSort(0).setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 106 | + when(facePackItemMapper.selectById(1L)).thenReturn(new ImFacePackItemDO()); | ||
| 107 | + when(facePackService.validateFacePackExists(10L)).thenReturn(new ImFacePackDO()); | ||
| 108 | + | ||
| 109 | + // 调用 | ||
| 110 | + service.updateFacePackItem(reqVO); | ||
| 111 | + | ||
| 112 | + // 断言 | ||
| 113 | + verify(facePackItemMapper).updateById(any(ImFacePackItemDO.class)); | ||
| 114 | + } | ||
| 115 | + | ||
| 116 | + @Test | ||
| 117 | + public void testUpdateFacePackItem_itemNotExists() { | ||
| 118 | + ImFacePackItemSaveReqVO reqVO = new ImFacePackItemSaveReqVO(); | ||
| 119 | + reqVO.setId(99L).setPackId(10L).setUrl("a.png").setWidth(100).setHeight(100).setSort(0) | ||
| 120 | + .setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 121 | + when(facePackItemMapper.selectById(99L)).thenReturn(null); | ||
| 122 | + | ||
| 123 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 124 | + () -> service.updateFacePackItem(reqVO)); | ||
| 125 | + assertEquals(FACE_PACK_ITEM_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 126 | + verify(facePackItemMapper, never()).updateById(any(ImFacePackItemDO.class)); | ||
| 127 | + } | ||
| 128 | + | ||
| 129 | + // ========== deleteFacePackItem ========== | ||
| 130 | + | ||
| 131 | + @Test | ||
| 132 | + public void testDeleteFacePackItem_success() { | ||
| 133 | + when(facePackItemMapper.selectById(1L)).thenReturn(new ImFacePackItemDO()); | ||
| 134 | + | ||
| 135 | + service.deleteFacePackItem(1L); | ||
| 136 | + | ||
| 137 | + verify(facePackItemMapper).deleteById(1L); | ||
| 138 | + } | ||
| 139 | + | ||
| 140 | + @Test | ||
| 141 | + public void testDeleteFacePackItem_notExists() { | ||
| 142 | + when(facePackItemMapper.selectById(99L)).thenReturn(null); | ||
| 143 | + | ||
| 144 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 145 | + () -> service.deleteFacePackItem(99L)); | ||
| 146 | + assertEquals(FACE_PACK_ITEM_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 147 | + verify(facePackItemMapper, never()).deleteById(anyLong()); | ||
| 148 | + } | ||
| 149 | + | ||
| 150 | + @Test | ||
| 151 | + public void testDeleteFacePackItemList_emptySkip() { | ||
| 152 | + service.deleteFacePackItemList(Collections.emptyList()); | ||
| 153 | + | ||
| 154 | + verify(facePackItemMapper, never()).deleteByIds(any()); | ||
| 155 | + } | ||
| 156 | + | ||
| 157 | + @Test | ||
| 158 | + public void testDeleteFacePackItemList_success() { | ||
| 159 | + service.deleteFacePackItemList(Arrays.asList(1L, 2L)); | ||
| 160 | + | ||
| 161 | + verify(facePackItemMapper).deleteByIds(Arrays.asList(1L, 2L)); | ||
| 162 | + } | ||
| 163 | + | ||
| 164 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/face/ImFacePackServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.face; | ||
| 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.manager.face.vo.pack.ImFacePackSaveReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFacePackDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.mysql.face.ImFacePackMapper; | ||
| 9 | +import org.junit.jupiter.api.Test; | ||
| 10 | +import org.mockito.ArgumentCaptor; | ||
| 11 | +import org.mockito.InjectMocks; | ||
| 12 | +import org.mockito.Mock; | ||
| 13 | + | ||
| 14 | +import java.util.Arrays; | ||
| 15 | +import java.util.Collections; | ||
| 16 | + | ||
| 17 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_PACK_HAS_ITEMS; | ||
| 18 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_PACK_NOT_EXISTS; | ||
| 19 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 20 | +import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 21 | +import static org.mockito.ArgumentMatchers.any; | ||
| 22 | +import static org.mockito.ArgumentMatchers.anyLong; | ||
| 23 | +import static org.mockito.Mockito.never; | ||
| 24 | +import static org.mockito.Mockito.verify; | ||
| 25 | +import static org.mockito.Mockito.when; | ||
| 26 | + | ||
| 27 | +/** | ||
| 28 | + * IM 表情包 Service 单元测试 | ||
| 29 | + * | ||
| 30 | + * @author 芋道源码 | ||
| 31 | + */ | ||
| 32 | +public class ImFacePackServiceImplTest extends BaseMockitoUnitTest { | ||
| 33 | + | ||
| 34 | + @InjectMocks | ||
| 35 | + private ImFacePackServiceImpl service; | ||
| 36 | + | ||
| 37 | + @Mock | ||
| 38 | + private ImFacePackMapper facePackMapper; | ||
| 39 | + @Mock | ||
| 40 | + private ImFacePackItemService facePackItemService; | ||
| 41 | + | ||
| 42 | + // ========== validateFacePackExists ========== | ||
| 43 | + | ||
| 44 | + @Test | ||
| 45 | + public void testValidateFacePackExists_notFound() { | ||
| 46 | + when(facePackMapper.selectById(1L)).thenReturn(null); | ||
| 47 | + | ||
| 48 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 49 | + () -> service.validateFacePackExists(1L)); | ||
| 50 | + assertEquals(FACE_PACK_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 51 | + } | ||
| 52 | + | ||
| 53 | + @Test | ||
| 54 | + public void testValidateFacePackExists_returnsDo() { | ||
| 55 | + ImFacePackDO pack = ImFacePackDO.builder().id(1L).name("猫主子").build(); | ||
| 56 | + when(facePackMapper.selectById(1L)).thenReturn(pack); | ||
| 57 | + | ||
| 58 | + assertEquals(pack, service.validateFacePackExists(1L)); | ||
| 59 | + } | ||
| 60 | + | ||
| 61 | + // ========== createFacePack ========== | ||
| 62 | + | ||
| 63 | + @Test | ||
| 64 | + public void testCreateFacePack_insert() { | ||
| 65 | + // 准备 | ||
| 66 | + ImFacePackSaveReqVO reqVO = new ImFacePackSaveReqVO(); | ||
| 67 | + reqVO.setName("猫主子").setIcon("icon.png").setSort(0).setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 68 | + | ||
| 69 | + // 调用 | ||
| 70 | + service.createFacePack(reqVO); | ||
| 71 | + | ||
| 72 | + // 断言:mapper.insert 被调用一次 | ||
| 73 | + ArgumentCaptor<ImFacePackDO> captor = ArgumentCaptor.forClass(ImFacePackDO.class); | ||
| 74 | + verify(facePackMapper).insert(captor.capture()); | ||
| 75 | + assertEquals("猫主子", captor.getValue().getName()); | ||
| 76 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | ||
| 77 | + } | ||
| 78 | + | ||
| 79 | + // ========== updateFacePack ========== | ||
| 80 | + | ||
| 81 | + @Test | ||
| 82 | + public void testUpdateFacePack_success() { | ||
| 83 | + // 准备:存在 | ||
| 84 | + ImFacePackSaveReqVO reqVO = new ImFacePackSaveReqVO(); | ||
| 85 | + reqVO.setId(1L).setName("新名").setSort(1).setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 86 | + when(facePackMapper.selectById(1L)).thenReturn(ImFacePackDO.builder().id(1L).build()); | ||
| 87 | + | ||
| 88 | + // 调用 | ||
| 89 | + service.updateFacePack(reqVO); | ||
| 90 | + | ||
| 91 | + // 断言 | ||
| 92 | + ArgumentCaptor<ImFacePackDO> captor = ArgumentCaptor.forClass(ImFacePackDO.class); | ||
| 93 | + verify(facePackMapper).updateById(captor.capture()); | ||
| 94 | + assertEquals(1L, captor.getValue().getId()); | ||
| 95 | + assertEquals("新名", captor.getValue().getName()); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + @Test | ||
| 99 | + public void testUpdateFacePack_notExists() { | ||
| 100 | + // 准备 | ||
| 101 | + ImFacePackSaveReqVO reqVO = new ImFacePackSaveReqVO(); | ||
| 102 | + reqVO.setId(99L).setName("x").setSort(0).setStatus(CommonStatusEnum.ENABLE.getStatus()); | ||
| 103 | + when(facePackMapper.selectById(99L)).thenReturn(null); | ||
| 104 | + | ||
| 105 | + // 调用 + 断言 | ||
| 106 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 107 | + () -> service.updateFacePack(reqVO)); | ||
| 108 | + assertEquals(FACE_PACK_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 109 | + verify(facePackMapper, never()).updateById(any(ImFacePackDO.class)); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + // ========== deleteFacePack ========== | ||
| 113 | + | ||
| 114 | + @Test | ||
| 115 | + public void testDeleteFacePack_success() { | ||
| 116 | + when(facePackMapper.selectById(1L)).thenReturn(ImFacePackDO.builder().id(1L).build()); | ||
| 117 | + when(facePackItemService.getFacePackItemCount(1L)).thenReturn(0L); | ||
| 118 | + | ||
| 119 | + service.deleteFacePack(1L); | ||
| 120 | + | ||
| 121 | + verify(facePackMapper).deleteById(1L); | ||
| 122 | + } | ||
| 123 | + | ||
| 124 | + @Test | ||
| 125 | + public void testDeleteFacePack_hasItems() { | ||
| 126 | + // 准备:包下仍有表情 | ||
| 127 | + when(facePackMapper.selectById(1L)).thenReturn(ImFacePackDO.builder().id(1L).build()); | ||
| 128 | + when(facePackItemService.getFacePackItemCount(1L)).thenReturn(3L); | ||
| 129 | + | ||
| 130 | + ServiceException exception = assertThrows(ServiceException.class, () -> service.deleteFacePack(1L)); | ||
| 131 | + assertEquals(FACE_PACK_HAS_ITEMS.getCode(), exception.getCode()); | ||
| 132 | + verify(facePackMapper, never()).deleteById(anyLong()); | ||
| 133 | + } | ||
| 134 | + | ||
| 135 | + @Test | ||
| 136 | + public void testDeleteFacePack_notExists() { | ||
| 137 | + when(facePackMapper.selectById(99L)).thenReturn(null); | ||
| 138 | + | ||
| 139 | + ServiceException exception = assertThrows(ServiceException.class, () -> service.deleteFacePack(99L)); | ||
| 140 | + assertEquals(FACE_PACK_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 141 | + } | ||
| 142 | + | ||
| 143 | + // ========== deleteFacePackList ========== | ||
| 144 | + | ||
| 145 | + @Test | ||
| 146 | + public void testDeleteFacePackList_emptySkip() { | ||
| 147 | + // 调用:空 ids 列表直接返回,不查询、不删除 | ||
| 148 | + service.deleteFacePackList(Collections.emptyList()); | ||
| 149 | + | ||
| 150 | + verify(facePackItemService, never()).getFacePackItemCount(any(java.util.Collection.class)); | ||
| 151 | + verify(facePackMapper, never()).deleteByIds(any()); | ||
| 152 | + } | ||
| 153 | + | ||
| 154 | + @Test | ||
| 155 | + public void testDeleteFacePackList_anyHasItemsRejectAll() { | ||
| 156 | + // 准备:批量中存在表情 | ||
| 157 | + when(facePackItemService.getFacePackItemCount(Arrays.asList(1L, 2L))).thenReturn(1L); | ||
| 158 | + | ||
| 159 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 160 | + () -> service.deleteFacePackList(Arrays.asList(1L, 2L))); | ||
| 161 | + assertEquals(FACE_PACK_HAS_ITEMS.getCode(), exception.getCode()); | ||
| 162 | + verify(facePackMapper, never()).deleteByIds(any()); | ||
| 163 | + } | ||
| 164 | + | ||
| 165 | + @Test | ||
| 166 | + public void testDeleteFacePackList_success() { | ||
| 167 | + // 准备 | ||
| 168 | + when(facePackItemService.getFacePackItemCount(Arrays.asList(1L, 2L))).thenReturn(0L); | ||
| 169 | + | ||
| 170 | + // 调用 | ||
| 171 | + service.deleteFacePackList(Arrays.asList(1L, 2L)); | ||
| 172 | + | ||
| 173 | + // 断言 | ||
| 174 | + verify(facePackMapper).deleteByIds(Arrays.asList(1L, 2L)); | ||
| 175 | + } | ||
| 176 | + | ||
| 177 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/face/ImFaceUserItemServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.face; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.exception.ServiceException; | ||
| 4 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 5 | +import cn.iocoder.yudao.module.im.controller.admin.face.vo.useritem.ImFaceUserItemSaveReqVO; | ||
| 6 | +import cn.iocoder.yudao.module.im.dal.dataobject.face.ImFaceUserItemDO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.mysql.face.ImFaceUserItemMapper; | ||
| 8 | +import org.junit.jupiter.api.Test; | ||
| 9 | +import org.mockito.ArgumentCaptor; | ||
| 10 | +import org.mockito.InjectMocks; | ||
| 11 | +import org.mockito.Mock; | ||
| 12 | + | ||
| 13 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_USER_ITEM_DUPLICATED; | ||
| 14 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_USER_ITEM_NOT_EXISTS; | ||
| 15 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FACE_USER_ITEM_NOT_OWN; | ||
| 16 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 17 | +import static org.junit.jupiter.api.Assertions.assertThrows; | ||
| 18 | +import static org.mockito.ArgumentMatchers.any; | ||
| 19 | +import static org.mockito.ArgumentMatchers.anyLong; | ||
| 20 | +import static org.mockito.Mockito.never; | ||
| 21 | +import static org.mockito.Mockito.verify; | ||
| 22 | +import static org.mockito.Mockito.when; | ||
| 23 | + | ||
| 24 | +/** | ||
| 25 | + * IM 用户私有表情 Service 单元测试 | ||
| 26 | + * | ||
| 27 | + * @author 芋道源码 | ||
| 28 | + */ | ||
| 29 | +public class ImFaceUserItemServiceImplTest extends BaseMockitoUnitTest { | ||
| 30 | + | ||
| 31 | + @InjectMocks | ||
| 32 | + private ImFaceUserItemServiceImpl service; | ||
| 33 | + | ||
| 34 | + @Mock | ||
| 35 | + private ImFaceUserItemMapper faceUserItemMapper; | ||
| 36 | + | ||
| 37 | + // ========== createFaceUserItem ========== | ||
| 38 | + | ||
| 39 | + @Test | ||
| 40 | + public void testCreateFaceUserItem_success() { | ||
| 41 | + // 准备 | ||
| 42 | + ImFaceUserItemSaveReqVO reqVO = new ImFaceUserItemSaveReqVO(); | ||
| 43 | + reqVO.setUrl("a.png").setName("doge").setWidth(100).setHeight(100); | ||
| 44 | + when(faceUserItemMapper.selectByUserIdAndUrl(1L, "a.png")).thenReturn(null); | ||
| 45 | + | ||
| 46 | + // 调用 | ||
| 47 | + service.createFaceUserItem(1L, reqVO); | ||
| 48 | + | ||
| 49 | + // 断言:写入用户编号 | ||
| 50 | + ArgumentCaptor<ImFaceUserItemDO> captor = ArgumentCaptor.forClass(ImFaceUserItemDO.class); | ||
| 51 | + verify(faceUserItemMapper).insert(captor.capture()); | ||
| 52 | + assertEquals(1L, captor.getValue().getUserId()); | ||
| 53 | + assertEquals("a.png", captor.getValue().getUrl()); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Test | ||
| 57 | + public void testCreateFaceUserItem_duplicateUrl() { | ||
| 58 | + // 准备:相同 (userId, url) 已存在 | ||
| 59 | + ImFaceUserItemSaveReqVO reqVO = new ImFaceUserItemSaveReqVO(); | ||
| 60 | + reqVO.setUrl("a.png").setWidth(100).setHeight(100); | ||
| 61 | + when(faceUserItemMapper.selectByUserIdAndUrl(1L, "a.png")) | ||
| 62 | + .thenReturn(new ImFaceUserItemDO().setUserId(1L)); | ||
| 63 | + | ||
| 64 | + // 调用 + 断言:抛重复异常,不落库 | ||
| 65 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 66 | + () -> service.createFaceUserItem(1L, reqVO)); | ||
| 67 | + assertEquals(FACE_USER_ITEM_DUPLICATED.getCode(), exception.getCode()); | ||
| 68 | + verify(faceUserItemMapper, never()).insert(any(ImFaceUserItemDO.class)); | ||
| 69 | + } | ||
| 70 | + | ||
| 71 | + // ========== deleteFaceUserItem(用户端) ========== | ||
| 72 | + | ||
| 73 | + @Test | ||
| 74 | + public void testDeleteFaceUserItem_userOwn_success() { | ||
| 75 | + // 准备:归属当前用户 | ||
| 76 | + when(faceUserItemMapper.selectById(10L)).thenReturn(new ImFaceUserItemDO().setUserId(1L)); | ||
| 77 | + | ||
| 78 | + // 调用 | ||
| 79 | + service.deleteFaceUserItem(1L, 10L); | ||
| 80 | + | ||
| 81 | + // 断言 | ||
| 82 | + verify(faceUserItemMapper).deleteById(10L); | ||
| 83 | + } | ||
| 84 | + | ||
| 85 | + @Test | ||
| 86 | + public void testDeleteFaceUserItem_notExists() { | ||
| 87 | + when(faceUserItemMapper.selectById(99L)).thenReturn(null); | ||
| 88 | + | ||
| 89 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 90 | + () -> service.deleteFaceUserItem(1L, 99L)); | ||
| 91 | + assertEquals(FACE_USER_ITEM_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 92 | + verify(faceUserItemMapper, never()).deleteById(anyLong()); | ||
| 93 | + } | ||
| 94 | + | ||
| 95 | + @Test | ||
| 96 | + public void testDeleteFaceUserItem_notOwn() { | ||
| 97 | + // 准备:表情归属 user 2,但 user 1 来删 | ||
| 98 | + when(faceUserItemMapper.selectById(10L)).thenReturn(new ImFaceUserItemDO().setUserId(2L)); | ||
| 99 | + | ||
| 100 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 101 | + () -> service.deleteFaceUserItem(1L, 10L)); | ||
| 102 | + assertEquals(FACE_USER_ITEM_NOT_OWN.getCode(), exception.getCode()); | ||
| 103 | + verify(faceUserItemMapper, never()).deleteById(anyLong()); | ||
| 104 | + } | ||
| 105 | + | ||
| 106 | + // ========== deleteFaceUserItem(管理后台 by id) ========== | ||
| 107 | + | ||
| 108 | + @Test | ||
| 109 | + public void testDeleteFaceUserItemAdmin_success() { | ||
| 110 | + when(faceUserItemMapper.selectById(10L)).thenReturn(new ImFaceUserItemDO().setUserId(2L)); | ||
| 111 | + | ||
| 112 | + service.deleteFaceUserItem(10L); | ||
| 113 | + | ||
| 114 | + verify(faceUserItemMapper).deleteById(10L); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + @Test | ||
| 118 | + public void testDeleteFaceUserItemAdmin_notExists() { | ||
| 119 | + when(faceUserItemMapper.selectById(99L)).thenReturn(null); | ||
| 120 | + | ||
| 121 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 122 | + () -> service.deleteFaceUserItem(99L)); | ||
| 123 | + assertEquals(FACE_USER_ITEM_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 124 | + } | ||
| 125 | + | ||
| 126 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendRequestServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.friend; | ||
| 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.friend.vo.request.ImFriendRequestApplyReqVO; | ||
| 7 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendDO; | ||
| 8 | +import cn.iocoder.yudao.module.im.dal.dataobject.friend.ImFriendRequestDO; | ||
| 9 | +import cn.iocoder.yudao.module.im.dal.mysql.friend.ImFriendRequestMapper; | ||
| 10 | +import cn.iocoder.yudao.module.im.enums.friend.ImFriendRequestHandleResultEnum; | ||
| 11 | +import cn.iocoder.yudao.module.im.enums.friend.ImFriendStateEnum; | ||
| 12 | +import cn.iocoder.yudao.module.im.framework.config.ImProperties; | ||
| 13 | +import cn.iocoder.yudao.module.im.service.websocket.ImWebSocketService; | ||
| 14 | +import cn.iocoder.yudao.module.im.service.websocket.dto.ImPrivateMessageDTO; | ||
| 15 | +import cn.iocoder.yudao.module.system.api.user.AdminUserApi; | ||
| 16 | +import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO; | ||
| 17 | +import org.junit.jupiter.api.Test; | ||
| 18 | +import org.mockito.ArgumentCaptor; | ||
| 19 | +import org.mockito.InjectMocks; | ||
| 20 | +import org.mockito.Mock; | ||
| 21 | + | ||
| 22 | +import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.*; | ||
| 23 | +import static org.junit.jupiter.api.Assertions.*; | ||
| 24 | +import static org.mockito.ArgumentMatchers.any; | ||
| 25 | +import static org.mockito.ArgumentMatchers.anyInt; | ||
| 26 | +import static org.mockito.ArgumentMatchers.anyLong; | ||
| 27 | +import static org.mockito.ArgumentMatchers.anyString; | ||
| 28 | +import static org.mockito.ArgumentMatchers.eq; | ||
| 29 | +import static org.mockito.Mockito.never; | ||
| 30 | +import static org.mockito.Mockito.verify; | ||
| 31 | +import static org.mockito.Mockito.when; | ||
| 32 | + | ||
| 33 | +/** | ||
| 34 | + * IM 好友申请 Service 单元测试 | ||
| 35 | + * | ||
| 36 | + * @author 芋道源码 | ||
| 37 | + */ | ||
| 38 | +public class ImFriendRequestServiceImplTest extends BaseMockitoUnitTest { | ||
| 39 | + | ||
| 40 | + @InjectMocks | ||
| 41 | + private ImFriendRequestServiceImpl friendRequestService; | ||
| 42 | + | ||
| 43 | + @Mock | ||
| 44 | + private ImFriendRequestMapper friendRequestMapper; | ||
| 45 | + @Mock | ||
| 46 | + private ImFriendService friendService; | ||
| 47 | + @Mock | ||
| 48 | + private ImWebSocketService websocketService; | ||
| 49 | + @Mock | ||
| 50 | + private ImProperties imProperties; | ||
| 51 | + @Mock | ||
| 52 | + private AdminUserApi adminUserApi; | ||
| 53 | + | ||
| 54 | + // ========== applyFriend ========== | ||
| 55 | + | ||
| 56 | + @Test | ||
| 57 | + public void testApplyFriend_addSelf() { | ||
| 58 | + // 准备:发起人 = 接收人 | ||
| 59 | + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); | ||
| 60 | + reqVO.setToUserId(1L); | ||
| 61 | + | ||
| 62 | + // 调用 + 断言 | ||
| 63 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 64 | + () -> friendRequestService.applyFriend(1L, reqVO)); | ||
| 65 | + assertEquals(FRIEND_ADD_SELF.getCode(), exception.getCode()); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + @Test | ||
| 69 | + public void testApplyFriend_alreadyFriend() { | ||
| 70 | + // 准备 | ||
| 71 | + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); | ||
| 72 | + reqVO.setToUserId(2L); | ||
| 73 | + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.FRIEND.getState()); | ||
| 74 | + | ||
| 75 | + // 调用 + 断言 | ||
| 76 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 77 | + () -> friendRequestService.applyFriend(1L, reqVO)); | ||
| 78 | + assertEquals(FRIEND_REQUEST_ALREADY_FRIEND.getCode(), exception.getCode()); | ||
| 79 | + } | ||
| 80 | + | ||
| 81 | + @Test | ||
| 82 | + public void testApplyFriend_blockedByPeer() { | ||
| 83 | + // 准备 | ||
| 84 | + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); | ||
| 85 | + reqVO.setToUserId(2L); | ||
| 86 | + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.BLOCKED.getState()); | ||
| 87 | + | ||
| 88 | + // 调用 + 断言 | ||
| 89 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 90 | + () -> friendRequestService.applyFriend(1L, reqVO)); | ||
| 91 | + assertEquals(FRIEND_REQUEST_BLOCKED_BY_PEER.getCode(), exception.getCode()); | ||
| 92 | + } | ||
| 93 | + | ||
| 94 | + @Test | ||
| 95 | + public void testApplyFriend_silentReAdd() { | ||
| 96 | + // 准备:单向好友 — 我已删除,但对方仍把我当好友 | ||
| 97 | + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); | ||
| 98 | + reqVO.setToUserId(2L).setDisplayName("老张").setAddSource(1); | ||
| 99 | + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState()); | ||
| 100 | + ImFriendDO peerFriend = ImFriendDO.builder().userId(2L).friendUserId(1L) | ||
| 101 | + .status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 102 | + when(friendService.getFriend(2L, 1L)).thenReturn(peerFriend); | ||
| 103 | + | ||
| 104 | + // 调用 | ||
| 105 | + ImFriendRequestDO result = friendRequestService.applyFriend(1L, reqVO); | ||
| 106 | + | ||
| 107 | + // 断言:走静默重新加好友,不落申请记录 | ||
| 108 | + assertNull(result); | ||
| 109 | + verify(friendService).silentReAddFriend(eq(1L), eq(2L), eq("老张"), eq(1)); | ||
| 110 | + verify(friendRequestMapper, never()).insert(any(ImFriendRequestDO.class)); | ||
| 111 | + verify(websocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); | ||
| 112 | + } | ||
| 113 | + | ||
| 114 | + @Test | ||
| 115 | + public void testApplyFriend_insertNew() { | ||
| 116 | + // 准备:双方都无关系,且无历史申请 | ||
| 117 | + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); | ||
| 118 | + reqVO.setToUserId(2L).setApplyContent("加个好友").setDisplayName("老张").setAddSource(1); | ||
| 119 | + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState()); | ||
| 120 | + when(friendService.getFriend(2L, 1L)).thenReturn(null); | ||
| 121 | + when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(null); | ||
| 122 | + when(adminUserApi.getUser(1L)).thenReturn(new AdminUserRespDTO().setNickname("张三").setAvatar("a.png")); | ||
| 123 | + when(imProperties.getFriend()).thenReturn(new ImProperties.Friend()); | ||
| 124 | + | ||
| 125 | + // 调用 | ||
| 126 | + ImFriendRequestDO result = friendRequestService.applyFriend(1L, reqVO); | ||
| 127 | + | ||
| 128 | + // 断言:落新申请记录 | ||
| 129 | + ArgumentCaptor<ImFriendRequestDO> captor = ArgumentCaptor.forClass(ImFriendRequestDO.class); | ||
| 130 | + verify(friendRequestMapper).insert(captor.capture()); | ||
| 131 | + ImFriendRequestDO saved = captor.getValue(); | ||
| 132 | + assertEquals(1L, saved.getFromUserId()); | ||
| 133 | + assertEquals(2L, saved.getToUserId()); | ||
| 134 | + assertEquals("加个好友", saved.getApplyContent()); | ||
| 135 | + assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), saved.getHandleResult()); | ||
| 136 | + assertSame(saved, result); | ||
| 137 | + // 断言:推送给接收方 | ||
| 138 | + verify(websocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + @Test | ||
| 142 | + public void testApplyFriend_reuseOldRequest() { | ||
| 143 | + // 准备:双方都无关系,但存在历史申请 — 走 reset 复用 | ||
| 144 | + ImFriendRequestApplyReqVO reqVO = new ImFriendRequestApplyReqVO(); | ||
| 145 | + reqVO.setToUserId(2L).setApplyContent("再来一次").setDisplayName("老张").setAddSource(2); | ||
| 146 | + when(friendService.getFriendState(1L, 2L)).thenReturn(ImFriendStateEnum.NONE.getState()); | ||
| 147 | + when(friendService.getFriend(2L, 1L)).thenReturn(null); | ||
| 148 | + ImFriendRequestDO old = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 149 | + .setHandleResult(ImFriendRequestHandleResultEnum.REFUSED.getResult()) | ||
| 150 | + .setHandleContent("旧拒绝").setApplyContent("旧内容"); | ||
| 151 | + when(friendRequestMapper.selectByFromUserIdAndToUserId(1L, 2L)).thenReturn(old); | ||
| 152 | + when(adminUserApi.getUser(1L)).thenReturn(null); | ||
| 153 | + when(imProperties.getFriend()).thenReturn(new ImProperties.Friend()); | ||
| 154 | + | ||
| 155 | + // 调用 | ||
| 156 | + ImFriendRequestDO result = friendRequestService.applyFriend(1L, reqVO); | ||
| 157 | + | ||
| 158 | + // 断言:复用旧记录,未触发 insert | ||
| 159 | + verify(friendRequestMapper).updateByIdReset(eq(100L), eq("再来一次"), eq("老张"), eq(2)); | ||
| 160 | + verify(friendRequestMapper, never()).insert(any(ImFriendRequestDO.class)); | ||
| 161 | + assertEquals(100L, result.getId()); | ||
| 162 | + assertEquals("再来一次", result.getApplyContent()); | ||
| 163 | + assertEquals(ImFriendRequestHandleResultEnum.UNHANDLED.getResult(), result.getHandleResult()); | ||
| 164 | + assertNull(result.getHandleContent()); | ||
| 165 | + assertNull(result.getHandleTime()); | ||
| 166 | + } | ||
| 167 | + | ||
| 168 | + // ========== agreeFriendRequest ========== | ||
| 169 | + | ||
| 170 | + @Test | ||
| 171 | + public void testAgreeFriendRequest_success() { | ||
| 172 | + // 准备 | ||
| 173 | + ImFriendRequestDO request = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 174 | + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); | ||
| 175 | + when(friendRequestMapper.selectById(100L)).thenReturn(request); | ||
| 176 | + when(friendRequestMapper.updateByIdAndHandleResult(eq(100L), | ||
| 177 | + eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), any(ImFriendRequestDO.class))).thenReturn(1); | ||
| 178 | + | ||
| 179 | + // 调用 | ||
| 180 | + friendRequestService.agreeFriendRequest(2L, 100L); | ||
| 181 | + | ||
| 182 | + // 断言:双向建立好友 + 推 APPROVED 给发起方 | ||
| 183 | + verify(friendService).becomeFriends(request); | ||
| 184 | + verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | ||
| 185 | + } | ||
| 186 | + | ||
| 187 | + @Test | ||
| 188 | + public void testAgreeFriendRequest_notExists() { | ||
| 189 | + // 准备:申请不存在 | ||
| 190 | + when(friendRequestMapper.selectById(100L)).thenReturn(null); | ||
| 191 | + | ||
| 192 | + // 调用 + 断言 | ||
| 193 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 194 | + () -> friendRequestService.agreeFriendRequest(2L, 100L)); | ||
| 195 | + assertEquals(FRIEND_REQUEST_NOT_EXISTS.getCode(), exception.getCode()); | ||
| 196 | + } | ||
| 197 | + | ||
| 198 | + @Test | ||
| 199 | + public void testAgreeFriendRequest_notToMe() { | ||
| 200 | + // 准备:操作人不是接收方 | ||
| 201 | + ImFriendRequestDO request = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 202 | + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); | ||
| 203 | + when(friendRequestMapper.selectById(100L)).thenReturn(request); | ||
| 204 | + | ||
| 205 | + // 调用 + 断言:3L 不是接收方 2L | ||
| 206 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 207 | + () -> friendRequestService.agreeFriendRequest(3L, 100L)); | ||
| 208 | + assertEquals(FRIEND_REQUEST_NOT_TO_ME.getCode(), exception.getCode()); | ||
| 209 | + } | ||
| 210 | + | ||
| 211 | + @Test | ||
| 212 | + public void testAgreeFriendRequest_alreadyHandled() { | ||
| 213 | + // 准备:申请已被处理 | ||
| 214 | + ImFriendRequestDO request = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 215 | + .setHandleResult(ImFriendRequestHandleResultEnum.AGREED.getResult()); | ||
| 216 | + when(friendRequestMapper.selectById(100L)).thenReturn(request); | ||
| 217 | + | ||
| 218 | + // 调用 + 断言 | ||
| 219 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 220 | + () -> friendRequestService.agreeFriendRequest(2L, 100L)); | ||
| 221 | + assertEquals(FRIEND_REQUEST_HANDLED.getCode(), exception.getCode()); | ||
| 222 | + } | ||
| 223 | + | ||
| 224 | + @Test | ||
| 225 | + public void testAgreeFriendRequest_concurrentCasFail() { | ||
| 226 | + // 准备:fail-fast 校验通过,但乐观锁 CAS 被并发抢先 | ||
| 227 | + ImFriendRequestDO request = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 228 | + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); | ||
| 229 | + when(friendRequestMapper.selectById(100L)).thenReturn(request); | ||
| 230 | + when(friendRequestMapper.updateByIdAndHandleResult(anyLong(), anyInt(), any(ImFriendRequestDO.class))).thenReturn(0); | ||
| 231 | + | ||
| 232 | + // 调用 + 断言:返回 FRIEND_REQUEST_HANDLED | ||
| 233 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 234 | + () -> friendRequestService.agreeFriendRequest(2L, 100L)); | ||
| 235 | + assertEquals(FRIEND_REQUEST_HANDLED.getCode(), exception.getCode()); | ||
| 236 | + verify(friendService, never()).becomeFriends(any(ImFriendRequestDO.class)); | ||
| 237 | + } | ||
| 238 | + | ||
| 239 | + // ========== refuseFriendRequest ========== | ||
| 240 | + | ||
| 241 | + @Test | ||
| 242 | + public void testRefuseFriendRequest_success() { | ||
| 243 | + // 准备 | ||
| 244 | + ImFriendRequestDO request = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 245 | + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); | ||
| 246 | + when(friendRequestMapper.selectById(100L)).thenReturn(request); | ||
| 247 | + when(friendRequestMapper.updateByIdAndHandleResult(eq(100L), | ||
| 248 | + eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), any(ImFriendRequestDO.class))).thenReturn(1); | ||
| 249 | + | ||
| 250 | + // 调用 | ||
| 251 | + friendRequestService.refuseFriendRequest(2L, 100L, "不认识"); | ||
| 252 | + | ||
| 253 | + // 断言:handleResult / handleContent 写入;推 REJECTED 给发起方 | ||
| 254 | + ArgumentCaptor<ImFriendRequestDO> captor = ArgumentCaptor.forClass(ImFriendRequestDO.class); | ||
| 255 | + verify(friendRequestMapper).updateByIdAndHandleResult(eq(100L), | ||
| 256 | + eq(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()), captor.capture()); | ||
| 257 | + assertEquals(ImFriendRequestHandleResultEnum.REFUSED.getResult(), captor.getValue().getHandleResult()); | ||
| 258 | + assertEquals("不认识", captor.getValue().getHandleContent()); | ||
| 259 | + verify(websocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | ||
| 260 | + } | ||
| 261 | + | ||
| 262 | + @Test | ||
| 263 | + public void testRefuseFriendRequest_concurrentCasFail() { | ||
| 264 | + // 准备:CAS 失败 | ||
| 265 | + ImFriendRequestDO request = new ImFriendRequestDO().setId(100L).setFromUserId(1L).setToUserId(2L) | ||
| 266 | + .setHandleResult(ImFriendRequestHandleResultEnum.UNHANDLED.getResult()); | ||
| 267 | + when(friendRequestMapper.selectById(100L)).thenReturn(request); | ||
| 268 | + when(friendRequestMapper.updateByIdAndHandleResult(anyLong(), anyInt(), any(ImFriendRequestDO.class))).thenReturn(0); | ||
| 269 | + | ||
| 270 | + // 调用 + 断言:抛 HANDLED,且不推 REJECTED | ||
| 271 | + ServiceException exception = assertThrows(ServiceException.class, | ||
| 272 | + () -> friendRequestService.refuseFriendRequest(2L, 100L, "x")); | ||
| 273 | + assertEquals(FRIEND_REQUEST_HANDLED.getCode(), exception.getCode()); | ||
| 274 | + verify(websocketService, never()).sendPrivateMessageAsync(anyLong(), any(ImPrivateMessageDTO.class)); | ||
| 275 | + } | ||
| 276 | + | ||
| 277 | + // ========== getMyFriendRequestList ========== | ||
| 278 | + | ||
| 279 | + @Test | ||
| 280 | + public void testGetMyFriendRequestList_delegate() { | ||
| 281 | + // 准备:mapper 返回 mock 数据 | ||
| 282 | + ImFriendRequestDO one = new ImFriendRequestDO().setId(1L); | ||
| 283 | + when(friendRequestMapper.selectMyList(1L, 99L, 20)).thenReturn(java.util.Collections.singletonList(one)); | ||
| 284 | + | ||
| 285 | + // 调用 + 断言:参数透传、结果原样返回 | ||
| 286 | + assertEquals(1, friendRequestService.getMyFriendRequestList(1L, 99L, 20).size()); | ||
| 287 | + verify(friendRequestMapper).selectMyList(1L, 99L, 20); | ||
| 288 | + } | ||
| 289 | + | ||
| 290 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/friend/ImFriendServiceImplTest.java
| @@ -22,6 +22,7 @@ import java.util.List; | @@ -22,6 +22,7 @@ import java.util.List; | ||
| 22 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; | 22 | import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.FRIEND_NOT_FRIEND; |
| 23 | import static org.junit.jupiter.api.Assertions.*; | 23 | import static org.junit.jupiter.api.Assertions.*; |
| 24 | import static org.mockito.ArgumentMatchers.any; | 24 | import static org.mockito.ArgumentMatchers.any; |
| 25 | +import static org.mockito.ArgumentMatchers.anyLong; | ||
| 25 | import static org.mockito.ArgumentMatchers.eq; | 26 | import static org.mockito.ArgumentMatchers.eq; |
| 26 | import static org.mockito.Mockito.never; | 27 | import static org.mockito.Mockito.never; |
| 27 | import static org.mockito.Mockito.verify; | 28 | import static org.mockito.Mockito.verify; |
| @@ -109,17 +110,15 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | @@ -109,17 +110,15 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 109 | 110 | ||
| 110 | @Test | 111 | @Test |
| 111 | public void testUpdateFriend_emptyRequest() { | 112 | public void testUpdateFriend_emptyRequest() { |
| 112 | - // 准备:silent 和 displayName 都不传 —— 直接返回,不打 SQL 也不发推送 | 113 | + // 准备:silent / displayName / pinned 都不传 —— 进入方法立刻返回,不查 mapper 也不发推送 |
| 113 | ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); | 114 | ImFriendUpdateReqVO reqVO = new ImFriendUpdateReqVO(); |
| 114 | reqVO.setFriendUserId(2L); | 115 | reqVO.setFriendUserId(2L); |
| 115 | - ImFriendDO friend = ImFriendDO.builder().id(100L).userId(1L).friendUserId(2L) | ||
| 116 | - .silent(false).status(CommonStatusEnum.ENABLE.getStatus()).build(); | ||
| 117 | - when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(friend); | ||
| 118 | 116 | ||
| 119 | // 调用 | 117 | // 调用 |
| 120 | friendService.updateFriend(1L, reqVO); | 118 | friendService.updateFriend(1L, reqVO); |
| 121 | 119 | ||
| 122 | - // 断言:没触发 SQL 更新 / 没发 WebSocket 推送 | 120 | + // 断言:没查记录、没触发 SQL 更新 / 没发 WebSocket 推送 |
| 121 | + verify(imFriendMapper, never()).selectByUserIdAndFriendUserId(anyLong(), anyLong()); | ||
| 123 | verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); | 122 | verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); |
| 124 | verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class)); | 123 | verify(imWebSocketService, never()).sendPrivateMessageAsync(any(Long.class), any(ImPrivateMessageDTO.class)); |
| 125 | } | 124 | } |
| @@ -127,18 +126,25 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | @@ -127,18 +126,25 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 127 | // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ========== | 126 | // ========== addFriend0(内部方法,被 becomeFriends / silentReAddFriend 调用) ========== |
| 128 | 127 | ||
| 129 | @Test | 128 | @Test |
| 130 | - public void testAddFriend0_existingEnabledSkipsUpdate() { | ||
| 131 | - // 准备:已存在且启用,第二步不应触发更新 | 129 | + public void testAddFriend0_existingEnabledResetsFields() { |
| 130 | + // 准备:已存在且启用 —— 走复用旧记录路径,重置 silent / pinned / blocked | ||
| 132 | ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) | 131 | ImFriendDO exists = ImFriendDO.builder().id(10L).userId(1L).friendUserId(2L) |
| 133 | - .status(CommonStatusEnum.ENABLE.getStatus()).build(); | 132 | + .status(CommonStatusEnum.ENABLE.getStatus()).silent(true).pinned(true).blocked(true).build(); |
| 134 | when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); | 133 | when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(exists); |
| 135 | 134 | ||
| 136 | // 调用 | 135 | // 调用 |
| 137 | friendService.addFriend0(1L, 2L, null, null); | 136 | friendService.addFriend0(1L, 2L, null, null); |
| 138 | 137 | ||
| 139 | - // 断言:不插入,也不更新 | 138 | + // 断言:不插入;走 update 重置 silent / pinned / blocked 为 false,并刷新 addTime |
| 140 | verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); | 139 | verify(imFriendMapper, never()).insert(any(ImFriendDO.class)); |
| 141 | - verify(imFriendMapper, never()).updateById(any(ImFriendDO.class)); | 140 | + ArgumentCaptor<ImFriendDO> captor = ArgumentCaptor.forClass(ImFriendDO.class); |
| 141 | + verify(imFriendMapper).updateById(captor.capture()); | ||
| 142 | + assertEquals(10L, captor.getValue().getId()); | ||
| 143 | + assertEquals(CommonStatusEnum.ENABLE.getStatus(), captor.getValue().getStatus()); | ||
| 144 | + assertFalse(captor.getValue().getSilent()); | ||
| 145 | + assertFalse(captor.getValue().getPinned()); | ||
| 146 | + assertFalse(captor.getValue().getBlocked()); | ||
| 147 | + assertNotNull(captor.getValue().getAddTime()); | ||
| 142 | } | 148 | } |
| 143 | 149 | ||
| 144 | @Test | 150 | @Test |
| @@ -160,15 +166,14 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | @@ -160,15 +166,14 @@ public class ImFriendServiceImplTest extends BaseMockitoUnitTest { | ||
| 160 | } | 166 | } |
| 161 | 167 | ||
| 162 | @Test | 168 | @Test |
| 163 | - public void testAddFriend0_duplicateKeySwallowed() { | ||
| 164 | - // 准备:mapper 抛并发冲突 | 169 | + public void testAddFriend0_duplicateKeyPropagates() { |
| 170 | + // 准备:mapper 抛并发冲突;极端并发下让异常向外抛,由外层事务回滚 | ||
| 165 | when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(null); | 171 | when(imFriendMapper.selectByUserIdAndFriendUserId(1L, 2L)).thenReturn(null); |
| 166 | when(imFriendMapper.insert(any(ImFriendDO.class))) | 172 | when(imFriendMapper.insert(any(ImFriendDO.class))) |
| 167 | .thenThrow(new DuplicateKeyException("concurrent insert")); | 173 | .thenThrow(new DuplicateKeyException("concurrent insert")); |
| 168 | 174 | ||
| 169 | - // 调用:应被吞掉,不抛异常 | ||
| 170 | - friendService.addFriend0(1L, 2L, null, null); | ||
| 171 | - | 175 | + // 调用 + 断言:异常向外抛 |
| 176 | + assertThrows(DuplicateKeyException.class, () -> friendService.addFriend0(1L, 2L, null, null)); | ||
| 172 | verify(imFriendMapper).insert(any(ImFriendDO.class)); | 177 | verify(imFriendMapper).insert(any(ImFriendDO.class)); |
| 173 | } | 178 | } |
| 174 | 179 |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupMemberServiceImplTest.java
| @@ -36,6 +36,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -36,6 +36,8 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 36 | private ImGroupMemberMapper groupMemberMapper; | 36 | private ImGroupMemberMapper groupMemberMapper; |
| 37 | @Mock | 37 | @Mock |
| 38 | private ImWebSocketService webSocketService; | 38 | private ImWebSocketService webSocketService; |
| 39 | + @Mock | ||
| 40 | + private cn.iocoder.yudao.module.im.service.message.ImGroupMessageService groupMessageService; | ||
| 39 | 41 | ||
| 40 | // ========== addGroupMember ========== | 42 | // ========== addGroupMember ========== |
| 41 | 43 | ||
| @@ -241,7 +243,11 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | @@ -241,7 +243,11 @@ public class ImGroupMemberServiceImplTest extends BaseMockitoUnitTest { | ||
| 241 | verify(groupMemberMapper).updateById(captor.capture()); | 243 | verify(groupMemberMapper).updateById(captor.capture()); |
| 242 | assertEquals(50L, captor.getValue().getId()); | 244 | assertEquals(50L, captor.getValue().getId()); |
| 243 | assertTrue(captor.getValue().getSilent()); | 245 | assertTrue(captor.getValue().getSilent()); |
| 244 | - verify(webSocketService).sendGroupMessageAsync(eq(1L), any(ImGroupMessageDTO.class)); | 246 | + // 公开字段昵称变化 → 全员广播 GROUP_MEMBER_NICKNAME_UPDATE |
| 247 | + verify(groupMessageService).sendGroupMessage(eq(1L), any(cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO.class)); | ||
| 248 | + // 个人字段 silent 变化 → 仅自己多端同步 GROUP_MEMBER_SETTING_UPDATE | ||
| 249 | + verify(groupMessageService).sendGroupMessage(eq(1L), eq(java.util.List.of(1L)), | ||
| 250 | + any(cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO.class)); | ||
| 245 | } | 251 | } |
| 246 | 252 | ||
| 247 | // ========== removeGroupMember ========== | 253 | // ========== removeGroupMember ========== |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/group/ImGroupServiceImplTest.java
| @@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; | @@ -18,6 +18,7 @@ import cn.iocoder.yudao.module.im.dal.mysql.group.ImGroupMapper; | ||
| 18 | import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; | 18 | import cn.iocoder.yudao.module.im.enums.group.ImGroupAddSourceEnum; |
| 19 | import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; | 19 | import cn.iocoder.yudao.module.im.enums.group.ImGroupMemberRoleEnum; |
| 20 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; | 20 | import cn.iocoder.yudao.module.im.enums.message.ImMessageTypeEnum; |
| 21 | +import cn.iocoder.yudao.module.im.framework.config.ImProperties; | ||
| 21 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; | 22 | import cn.iocoder.yudao.module.im.service.friend.ImFriendService; |
| 22 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; | 23 | import cn.iocoder.yudao.module.im.service.message.ImGroupMessageService; |
| 23 | import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; | 24 | import cn.iocoder.yudao.module.im.service.message.dto.ImGroupMessageSendDTO; |
| @@ -30,6 +31,7 @@ import org.mockito.ArgumentCaptor; | @@ -30,6 +31,7 @@ import org.mockito.ArgumentCaptor; | ||
| 30 | import org.mockito.InjectMocks; | 31 | import org.mockito.InjectMocks; |
| 31 | import org.mockito.Mock; | 32 | import org.mockito.Mock; |
| 32 | import org.mockito.MockedStatic; | 33 | import org.mockito.MockedStatic; |
| 34 | +import org.mockito.Spy; | ||
| 33 | 35 | ||
| 34 | import java.time.LocalDateTime; | 36 | import java.time.LocalDateTime; |
| 35 | import java.util.ArrayList; | 37 | import java.util.ArrayList; |
| @@ -66,6 +68,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | @@ -66,6 +68,8 @@ public class ImGroupServiceImplTest extends BaseMockitoUnitTest { | ||
| 66 | private ImGroupRequestService groupRequestService; | 68 | private ImGroupRequestService groupRequestService; |
| 67 | @Mock | 69 | @Mock |
| 68 | private AdminUserApi adminUserApi; | 70 | private AdminUserApi adminUserApi; |
| 71 | + @Spy | ||
| 72 | + private ImProperties imProperties = new ImProperties(); | ||
| 69 | 73 | ||
| 70 | // ========== createGroup ========== | 74 | // ========== createGroup ========== |
| 71 | 75 |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImGroupMessageServiceImplTest.java
| @@ -68,7 +68,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -68,7 +68,7 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 68 | ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO(); | 68 | ImGroupMessageSendReqVO reqVO = new ImGroupMessageSendReqVO(); |
| 69 | reqVO.setClientMessageId("test-uuid-group-001"); | 69 | reqVO.setClientMessageId("test-uuid-group-001"); |
| 70 | reqVO.setGroupId(10L); | 70 | reqVO.setGroupId(10L); |
| 71 | - reqVO.setType(0); // TEXT | 71 | + reqVO.setType(ImMessageTypeEnum.TEXT.getType()); |
| 72 | reqVO.setContent("{\"content\":\"群聊你好\"}"); | 72 | reqVO.setContent("{\"content\":\"群聊你好\"}"); |
| 73 | return reqVO; | 73 | return reqVO; |
| 74 | } | 74 | } |
| @@ -1062,9 +1062,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -1062,9 +1062,9 @@ public class ImGroupMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 1062 | 1062 | ||
| 1063 | @Test | 1063 | @Test |
| 1064 | public void testSendGroupMessage_dto_nonPersistentTypeNotInserted() { | 1064 | public void testSendGroupMessage_dto_nonPersistentTypeNotInserted() { |
| 1065 | - // 准备:persistent=false 类型(GROUP_CREATE)→ 不入库,仅推送 | 1065 | + // 准备:persistent=false 类型(RECEIPT 回执)→ 不入库,仅推送 |
| 1066 | ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() | 1066 | ImGroupMessageSendDTO dto = new ImGroupMessageSendDTO() |
| 1067 | - .setGroupId(10L).setType(ImMessageTypeEnum.GROUP_CREATE.getType()); | 1067 | + .setGroupId(10L).setType(ImMessageTypeEnum.RECEIPT.getType()); |
| 1068 | when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L)); | 1068 | when(groupMemberService.getActiveGroupMemberUserIdsByGroupId(10L)).thenReturn(List.of(1L, 2L)); |
| 1069 | 1069 | ||
| 1070 | groupMessageService.sendGroupMessage(1L, dto); | 1070 | groupMessageService.sendGroupMessage(1L, dto); |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/message/ImPrivateMessageServiceImplTest.java
| @@ -56,7 +56,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -56,7 +56,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 56 | ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); | 56 | ImPrivateMessageSendReqVO reqVO = new ImPrivateMessageSendReqVO(); |
| 57 | reqVO.setClientMessageId("test-uuid-001"); | 57 | reqVO.setClientMessageId("test-uuid-001"); |
| 58 | reqVO.setReceiverId(2L); | 58 | reqVO.setReceiverId(2L); |
| 59 | - reqVO.setType(0); // TEXT | 59 | + reqVO.setType(ImMessageTypeEnum.TEXT.getType()); |
| 60 | reqVO.setContent("{\"content\":\"你好\"}"); | 60 | reqVO.setContent("{\"content\":\"你好\"}"); |
| 61 | return reqVO; | 61 | return reqVO; |
| 62 | } | 62 | } |
| @@ -82,7 +82,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -82,7 +82,7 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 82 | assertNotNull(result); | 82 | assertNotNull(result); |
| 83 | assertEquals(1L, result.getSenderId()); | 83 | assertEquals(1L, result.getSenderId()); |
| 84 | assertEquals(2L, result.getReceiverId()); | 84 | assertEquals(2L, result.getReceiverId()); |
| 85 | - assertEquals(0, result.getType()); | 85 | + assertEquals(ImMessageTypeEnum.TEXT.getType(), result.getType()); |
| 86 | assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus()); | 86 | assertEquals(ImMessageStatusEnum.UNREAD.getStatus(), result.getStatus()); |
| 87 | assertNotNull(result.getSendTime()); | 87 | assertNotNull(result.getSendTime()); |
| 88 | 88 | ||
| @@ -395,15 +395,15 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | @@ -395,15 +395,15 @@ public class ImPrivateMessageServiceImplTest extends BaseMockitoUnitTest { | ||
| 395 | 395 | ||
| 396 | @Test | 396 | @Test |
| 397 | public void testSendPrivateMessage_dto_nonPersistentTypeNotInserted() { | 397 | public void testSendPrivateMessage_dto_nonPersistentTypeNotInserted() { |
| 398 | - // 准备:persistent=false 类型(FRIEND_ADD)→ 不入库,仅推送 | 398 | + // 准备:persistent=false 类型(FRIEND_DELETE 通知)→ 不入库;仅推 sender 多端,receiver 不感知 |
| 399 | ImPrivateMessageSendDTO dto = new ImPrivateMessageSendDTO() | 399 | ImPrivateMessageSendDTO dto = new ImPrivateMessageSendDTO() |
| 400 | - .setReceiverId(2L).setType(ImMessageTypeEnum.FRIEND_ADD.getType()); | 400 | + .setReceiverId(2L).setType(ImMessageTypeEnum.FRIEND_DELETE.getType()); |
| 401 | 401 | ||
| 402 | privateMessageService.sendPrivateMessage(1L, dto); | 402 | privateMessageService.sendPrivateMessage(1L, dto); |
| 403 | 403 | ||
| 404 | verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class)); | 404 | verify(privateMessageMapper, never()).insert(any(ImPrivateMessageDO.class)); |
| 405 | verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); | 405 | verify(imWebSocketService).sendPrivateMessageAsync(eq(1L), any(ImPrivateMessageDTO.class)); |
| 406 | - verify(imWebSocketService).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); | 406 | + verify(imWebSocketService, never()).sendPrivateMessageAsync(eq(2L), any(ImPrivateMessageDTO.class)); |
| 407 | } | 407 | } |
| 408 | 408 | ||
| 409 | // ========== getPrivateMessageList ========== | 409 | // ========== getPrivateMessageList ========== |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/service/statistics/ImStatisticsManagerServiceImplTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.service.statistics; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest; | ||
| 4 | +import cn.iocoder.yudao.module.im.dal.mysql.statistics.ImStatisticsManagerMapper; | ||
| 5 | +import org.junit.jupiter.api.Test; | ||
| 6 | +import org.mockito.InjectMocks; | ||
| 7 | +import org.mockito.Mock; | ||
| 8 | + | ||
| 9 | +import java.time.LocalDate; | ||
| 10 | +import java.time.LocalDateTime; | ||
| 11 | +import java.util.Arrays; | ||
| 12 | +import java.util.HashMap; | ||
| 13 | +import java.util.Map; | ||
| 14 | + | ||
| 15 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 16 | +import static org.mockito.ArgumentMatchers.any; | ||
| 17 | +import static org.mockito.ArgumentMatchers.eq; | ||
| 18 | +import static org.mockito.Mockito.when; | ||
| 19 | + | ||
| 20 | +/** | ||
| 21 | + * IM 数据看板 Service 单元测试 | ||
| 22 | + * | ||
| 23 | + * @author 芋道源码 | ||
| 24 | + */ | ||
| 25 | +public class ImStatisticsManagerServiceImplTest extends BaseMockitoUnitTest { | ||
| 26 | + | ||
| 27 | + private static final LocalDateTime BEGIN = LocalDateTime.of(2026, 1, 1, 0, 0); | ||
| 28 | + private static final LocalDateTime END = LocalDateTime.of(2026, 2, 1, 0, 0); | ||
| 29 | + | ||
| 30 | + @InjectMocks | ||
| 31 | + private ImStatisticsManagerServiceImpl service; | ||
| 32 | + | ||
| 33 | + @Mock | ||
| 34 | + private ImStatisticsManagerMapper statisticsMapper; | ||
| 35 | + | ||
| 36 | + // ========== 单值委托 ========== | ||
| 37 | + | ||
| 38 | + @Test | ||
| 39 | + public void testGetTotalUserCount_delegate() { | ||
| 40 | + when(statisticsMapper.selectTotalUserCount()).thenReturn(99L); | ||
| 41 | + assertEquals(99L, service.getTotalUserCount()); | ||
| 42 | + } | ||
| 43 | + | ||
| 44 | + @Test | ||
| 45 | + public void testGetNewUserCount_delegate() { | ||
| 46 | + when(statisticsMapper.selectNewUserCount(BEGIN, END)).thenReturn(10L); | ||
| 47 | + assertEquals(10L, service.getNewUserCount(BEGIN, END)); | ||
| 48 | + } | ||
| 49 | + | ||
| 50 | + @Test | ||
| 51 | + public void testGetActiveUserCount_delegate() { | ||
| 52 | + when(statisticsMapper.selectActiveUserCount(BEGIN, END)).thenReturn(5L); | ||
| 53 | + assertEquals(5L, service.getActiveUserCount(BEGIN, END)); | ||
| 54 | + } | ||
| 55 | + | ||
| 56 | + @Test | ||
| 57 | + public void testGetTotalGroupCount_delegate() { | ||
| 58 | + when(statisticsMapper.selectTotalGroupCount()).thenReturn(33L); | ||
| 59 | + assertEquals(33L, service.getTotalGroupCount()); | ||
| 60 | + } | ||
| 61 | + | ||
| 62 | + @Test | ||
| 63 | + public void testGetPrivateMessageCount_delegate() { | ||
| 64 | + when(statisticsMapper.selectPrivateMessageCount(BEGIN, END)).thenReturn(123L); | ||
| 65 | + assertEquals(123L, service.getPrivateMessageCount(BEGIN, END)); | ||
| 66 | + } | ||
| 67 | + | ||
| 68 | + @Test | ||
| 69 | + public void testGetGroupMessageCount_delegate() { | ||
| 70 | + when(statisticsMapper.selectGroupMessageCount(BEGIN, END)).thenReturn(456L); | ||
| 71 | + assertEquals(456L, service.getGroupMessageCount(BEGIN, END)); | ||
| 72 | + } | ||
| 73 | + | ||
| 74 | + // ========== 每日序列 toDailyCountMap ========== | ||
| 75 | + | ||
| 76 | + @Test | ||
| 77 | + public void testGetNewUserDailyCountMap_dateConvert() { | ||
| 78 | + // 准备:date 字段以 SQL Date / String 混入,count 为 Long | ||
| 79 | + when(statisticsMapper.selectNewUserDailyCount(eq(BEGIN), eq(END))).thenReturn(Arrays.asList( | ||
| 80 | + row("date", java.sql.Date.valueOf("2026-01-10"), "count", 5L), | ||
| 81 | + row("date", "2026-01-11", "count", 8L))); | ||
| 82 | + | ||
| 83 | + // 调用 | ||
| 84 | + Map<LocalDateTime, Long> result = service.getNewUserDailyCountMap(BEGIN, END); | ||
| 85 | + | ||
| 86 | + // 断言:date → LocalDateTime 起始零点 | ||
| 87 | + assertEquals(2, result.size()); | ||
| 88 | + assertEquals(5L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay())); | ||
| 89 | + assertEquals(8L, result.get(LocalDate.of(2026, 1, 11).atStartOfDay())); | ||
| 90 | + } | ||
| 91 | + | ||
| 92 | + @Test | ||
| 93 | + public void testGetActiveUserDailyCountMap_dateConvert() { | ||
| 94 | + when(statisticsMapper.selectActiveUserDailyCount(any(), any())).thenReturn(Arrays.asList( | ||
| 95 | + row("date", "2026-01-10", "count", 3L))); | ||
| 96 | + | ||
| 97 | + Map<LocalDateTime, Long> result = service.getActiveUserDailyCountMap(BEGIN, END); | ||
| 98 | + assertEquals(3L, result.get(LocalDate.of(2026, 1, 10).atStartOfDay())); | ||
| 99 | + } | ||
| 100 | + | ||
| 101 | + @Test | ||
| 102 | + public void testGetPrivateMessageDailyCountMap_dateConvert() { | ||
| 103 | + when(statisticsMapper.selectPrivateMessageDailyCount(any(), any())).thenReturn(Arrays.asList( | ||
| 104 | + row("date", "2026-01-10", "count", 7L))); | ||
| 105 | + assertEquals(7L, service.getPrivateMessageDailyCountMap(BEGIN, END) | ||
| 106 | + .get(LocalDate.of(2026, 1, 10).atStartOfDay())); | ||
| 107 | + } | ||
| 108 | + | ||
| 109 | + @Test | ||
| 110 | + public void testGetGroupMessageDailyCountMap_dateConvert() { | ||
| 111 | + when(statisticsMapper.selectGroupMessageDailyCount(any(), any())).thenReturn(Arrays.asList( | ||
| 112 | + row("date", "2026-01-10", "count", 9L))); | ||
| 113 | + assertEquals(9L, service.getGroupMessageDailyCountMap(BEGIN, END) | ||
| 114 | + .get(LocalDate.of(2026, 1, 10).atStartOfDay())); | ||
| 115 | + } | ||
| 116 | + | ||
| 117 | + // ========== 分桶 / 分布 ========== | ||
| 118 | + | ||
| 119 | + @Test | ||
| 120 | + public void testGetGroupSizeCountMap() { | ||
| 121 | + when(statisticsMapper.selectGroupSizeDistribution()).thenReturn(Arrays.asList( | ||
| 122 | + row("range", "1-9 人", "count", 3L), | ||
| 123 | + row("range", "10-49 人", "count", 2L))); | ||
| 124 | + | ||
| 125 | + Map<String, Long> result = service.getGroupSizeCountMap(); | ||
| 126 | + assertEquals(3L, result.get("1-9 人")); | ||
| 127 | + assertEquals(2L, result.get("10-49 人")); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @Test | ||
| 131 | + public void testGetMessageTypeCountMap() { | ||
| 132 | + when(statisticsMapper.selectMessageTypeDistribution(BEGIN, END)).thenReturn(Arrays.asList( | ||
| 133 | + row("type", 101, "count", 8L), | ||
| 134 | + row("type", 102, "count", 1L))); | ||
| 135 | + | ||
| 136 | + Map<Integer, Long> result = service.getMessageTypeCountMap(BEGIN, END); | ||
| 137 | + assertEquals(8L, result.get(101)); | ||
| 138 | + assertEquals(1L, result.get(102)); | ||
| 139 | + } | ||
| 140 | + | ||
| 141 | + @Test | ||
| 142 | + public void testGetTopSenderCountMap_passesLimit() { | ||
| 143 | + when(statisticsMapper.selectTopSenders(BEGIN, END, 3)).thenReturn(Arrays.asList( | ||
| 144 | + row("userId", 1L, "messageCount", 10L), | ||
| 145 | + row("userId", 2L, "messageCount", 5L))); | ||
| 146 | + | ||
| 147 | + Map<Long, Long> result = service.getTopSenderCountMap(BEGIN, END, 3); | ||
| 148 | + assertEquals(10L, result.get(1L)); | ||
| 149 | + assertEquals(5L, result.get(2L)); | ||
| 150 | + } | ||
| 151 | + | ||
| 152 | + // ========== 工具 ========== | ||
| 153 | + | ||
| 154 | + private static Map<String, Object> row(String k1, Object v1, String k2, Object v2) { | ||
| 155 | + Map<String, Object> map = new HashMap<>(); | ||
| 156 | + map.put(k1, v1); | ||
| 157 | + map.put(k2, v2); | ||
| 158 | + return map; | ||
| 159 | + } | ||
| 160 | + | ||
| 161 | +} |
yudao-module-im/src/test/java/cn/iocoder/yudao/module/im/util/ImMessageUtilsTest.java
0 → 100644
| 1 | +package cn.iocoder.yudao.module.im.util; | ||
| 2 | + | ||
| 3 | +import cn.iocoder.yudao.framework.common.util.json.JsonUtils; | ||
| 4 | +import cn.iocoder.yudao.module.im.service.websocket.dto.message.QuoteMessage; | ||
| 5 | +import org.junit.jupiter.api.Test; | ||
| 6 | + | ||
| 7 | +import java.util.Map; | ||
| 8 | + | ||
| 9 | +import static org.junit.jupiter.api.Assertions.assertEquals; | ||
| 10 | +import static org.junit.jupiter.api.Assertions.assertFalse; | ||
| 11 | +import static org.junit.jupiter.api.Assertions.assertNull; | ||
| 12 | +import static org.junit.jupiter.api.Assertions.assertTrue; | ||
| 13 | + | ||
| 14 | +/** | ||
| 15 | + * IM 消息内容工具类单元测试 | ||
| 16 | + * | ||
| 17 | + * @author 芋道源码 | ||
| 18 | + */ | ||
| 19 | +public class ImMessageUtilsTest { | ||
| 20 | + | ||
| 21 | + // ========== parseQuoteMessageId ========== | ||
| 22 | + | ||
| 23 | + @Test | ||
| 24 | + public void testParseQuoteMessageId_valid() { | ||
| 25 | + // 准备:content 含合法 quote 字段 | ||
| 26 | + String content = "{\"text\":\"你好\",\"quote\":{\"messageId\":1001,\"senderId\":2,\"type\":1}}"; | ||
| 27 | + | ||
| 28 | + // 调用 + 断言 | ||
| 29 | + assertEquals(1001L, ImMessageUtils.parseQuoteMessageId(content)); | ||
| 30 | + } | ||
| 31 | + | ||
| 32 | + @Test | ||
| 33 | + public void testParseQuoteMessageId_invalidJson() { | ||
| 34 | + // 准备:content 非合法 JSON | ||
| 35 | + // 调用 + 断言:解析失败返回 null | ||
| 36 | + assertNull(ImMessageUtils.parseQuoteMessageId("not a json")); | ||
| 37 | + } | ||
| 38 | + | ||
| 39 | + @Test | ||
| 40 | + public void testParseQuoteMessageId_noQuoteField() { | ||
| 41 | + // 准备:content 无 quote 字段 | ||
| 42 | + String content = "{\"text\":\"你好\"}"; | ||
| 43 | + | ||
| 44 | + // 调用 + 断言 | ||
| 45 | + assertNull(ImMessageUtils.parseQuoteMessageId(content)); | ||
| 46 | + } | ||
| 47 | + | ||
| 48 | + @Test | ||
| 49 | + public void testParseQuoteMessageId_quoteIsNotMap() { | ||
| 50 | + // 准备:quote 字段不是对象 | ||
| 51 | + String content = "{\"text\":\"你好\",\"quote\":\"oops\"}"; | ||
| 52 | + | ||
| 53 | + // 调用 + 断言 | ||
| 54 | + assertNull(ImMessageUtils.parseQuoteMessageId(content)); | ||
| 55 | + } | ||
| 56 | + | ||
| 57 | + @Test | ||
| 58 | + public void testParseQuoteMessageId_messageIdMissing() { | ||
| 59 | + // 准备:quote 对象内无 messageId | ||
| 60 | + String content = "{\"quote\":{\"senderId\":2}}"; | ||
| 61 | + | ||
| 62 | + // 调用 + 断言 | ||
| 63 | + assertNull(ImMessageUtils.parseQuoteMessageId(content)); | ||
| 64 | + } | ||
| 65 | + | ||
| 66 | + // ========== appendQuote ========== | ||
| 67 | + | ||
| 68 | + @Test | ||
| 69 | + public void testAppendQuote_existingContent() { | ||
| 70 | + // 准备:已有 text 字段的 content | ||
| 71 | + String content = "{\"text\":\"你好\"}"; | ||
| 72 | + QuoteMessage quote = new QuoteMessage().setMessageId(1001L).setSenderId(2L).setType(1).setContent("{}"); | ||
| 73 | + | ||
| 74 | + // 调用 | ||
| 75 | + String result = ImMessageUtils.appendQuote(content, quote); | ||
| 76 | + | ||
| 77 | + // 断言:保留原字段,注入 quote | ||
| 78 | + Map<String, Object> map = JsonUtils.parseMap(result); | ||
| 79 | + assertEquals("你好", map.get("text")); | ||
| 80 | + assertTrue(map.get("quote") instanceof Map); | ||
| 81 | + assertEquals(1001, ((Map<?, ?>) map.get("quote")).get("messageId")); | ||
| 82 | + } | ||
| 83 | + | ||
| 84 | + @Test | ||
| 85 | + public void testAppendQuote_blankContent() { | ||
| 86 | + // 准备:content 为空字符串,无法解析为 map | ||
| 87 | + QuoteMessage quote = new QuoteMessage().setMessageId(1001L); | ||
| 88 | + | ||
| 89 | + // 调用 | ||
| 90 | + String result = ImMessageUtils.appendQuote("", quote); | ||
| 91 | + | ||
| 92 | + // 断言:内部新建 map,仅含 quote | ||
| 93 | + Map<String, Object> map = JsonUtils.parseMap(result); | ||
| 94 | + assertEquals(1, map.size()); | ||
| 95 | + assertTrue(map.containsKey("quote")); | ||
| 96 | + } | ||
| 97 | + | ||
| 98 | + @Test | ||
| 99 | + public void testAppendQuote_overwriteExistingQuote() { | ||
| 100 | + // 准备:content 已经有一个旧 quote,期望被新 quote 覆盖 | ||
| 101 | + String content = "{\"text\":\"hi\",\"quote\":{\"messageId\":1}}"; | ||
| 102 | + QuoteMessage quote = new QuoteMessage().setMessageId(9999L).setSenderId(2L).setType(1); | ||
| 103 | + | ||
| 104 | + // 调用 | ||
| 105 | + String result = ImMessageUtils.appendQuote(content, quote); | ||
| 106 | + | ||
| 107 | + // 断言:quote.messageId 被覆盖为新值 | ||
| 108 | + Map<String, Object> map = JsonUtils.parseMap(result); | ||
| 109 | + assertEquals(9999, ((Map<?, ?>) map.get("quote")).get("messageId")); | ||
| 110 | + } | ||
| 111 | + | ||
| 112 | + // ========== removeQuote ========== | ||
| 113 | + | ||
| 114 | + @Test | ||
| 115 | + public void testRemoveQuote_blankContent() { | ||
| 116 | + // 调用 + 断言:空内容原样返回 | ||
| 117 | + assertEquals("", ImMessageUtils.removeQuote("")); | ||
| 118 | + assertNull(ImMessageUtils.removeQuote(null)); | ||
| 119 | + } | ||
| 120 | + | ||
| 121 | + @Test | ||
| 122 | + public void testRemoveQuote_noQuoteField() { | ||
| 123 | + // 准备:content 字符串里没有 quote 关键字,提前返回原值 | ||
| 124 | + String content = "{\"text\":\"你好\"}"; | ||
| 125 | + | ||
| 126 | + // 调用 + 断言:同一引用直接返回 | ||
| 127 | + assertEquals(content, ImMessageUtils.removeQuote(content)); | ||
| 128 | + } | ||
| 129 | + | ||
| 130 | + @Test | ||
| 131 | + public void testRemoveQuote_withQuote() { | ||
| 132 | + // 准备:含 quote 的 content | ||
| 133 | + String content = "{\"text\":\"你好\",\"quote\":{\"messageId\":1}}"; | ||
| 134 | + | ||
| 135 | + // 调用 | ||
| 136 | + String result = ImMessageUtils.removeQuote(content); | ||
| 137 | + | ||
| 138 | + // 断言:quote 被移除,保留其它字段 | ||
| 139 | + Map<String, Object> map = JsonUtils.parseMap(result); | ||
| 140 | + assertFalse(map.containsKey("quote")); | ||
| 141 | + assertEquals("你好", map.get("text")); | ||
| 142 | + } | ||
| 143 | + | ||
| 144 | + @Test | ||
| 145 | + public void testRemoveQuote_invalidJsonContainingQuoteToken() { | ||
| 146 | + // 准备:字符串包含 quote 字面量但解析失败 | ||
| 147 | + String content = "not-a-json-but-has-\"quote\"-token"; | ||
| 148 | + | ||
| 149 | + // 调用 + 断言:解析失败返回原值 | ||
| 150 | + assertEquals(content, ImMessageUtils.removeQuote(content)); | ||
| 151 | + } | ||
| 152 | + | ||
| 153 | + // ========== buildQuote ========== | ||
| 154 | + | ||
| 155 | + @Test | ||
| 156 | + public void testBuildQuote_stripsNestedQuote() { | ||
| 157 | + // 准备:被引用消息本身也含有 quote 字段;防止嵌套 | ||
| 158 | + String originalContent = "{\"text\":\"被引用\",\"quote\":{\"messageId\":777}}"; | ||
| 159 | + | ||
| 160 | + // 调用 | ||
| 161 | + QuoteMessage quote = ImMessageUtils.buildQuote(1001L, 2L, 1, originalContent); | ||
| 162 | + | ||
| 163 | + // 断言:基础字段透传,content 已被剥离 quote | ||
| 164 | + assertEquals(1001L, quote.getMessageId()); | ||
| 165 | + assertEquals(2L, quote.getSenderId()); | ||
| 166 | + assertEquals(1, quote.getType()); | ||
| 167 | + Map<String, Object> contentMap = JsonUtils.parseMap(quote.getContent()); | ||
| 168 | + assertFalse(contentMap.containsKey("quote")); | ||
| 169 | + assertEquals("被引用", contentMap.get("text")); | ||
| 170 | + } | ||
| 171 | + | ||
| 172 | + @Test | ||
| 173 | + public void testBuildQuote_originalWithoutQuote() { | ||
| 174 | + // 准备:被引用消息原本就没 quote 字段 | ||
| 175 | + String originalContent = "{\"text\":\"hi\"}"; | ||
| 176 | + | ||
| 177 | + // 调用 | ||
| 178 | + QuoteMessage quote = ImMessageUtils.buildQuote(1001L, 2L, 1, originalContent); | ||
| 179 | + | ||
| 180 | + // 断言:content 原样保留 | ||
| 181 | + assertEquals(originalContent, quote.getContent()); | ||
| 182 | + } | ||
| 183 | + | ||
| 184 | +} |
yudao-module-im/src/test/resources/application-unit-test.yaml
| @@ -9,7 +9,7 @@ spring: | @@ -9,7 +9,7 @@ spring: | ||
| 9 | # 数据源配置项 | 9 | # 数据源配置项 |
| 10 | datasource: | 10 | datasource: |
| 11 | name: ruoyi-vue-pro | 11 | name: ruoyi-vue-pro |
| 12 | - url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 | 12 | + url: jdbc:h2:mem:testdb;MODE=MYSQL;DATABASE_TO_UPPER=false;NON_KEYWORDS=value,day; # MODE 使用 MySQL 模式;DATABASE_TO_UPPER 配置表和字段使用小写 |
| 13 | driver-class-name: org.h2.Driver | 13 | driver-class-name: org.h2.Driver |
| 14 | username: sa | 14 | username: sa |
| 15 | password: | 15 | password: |
yudao-module-im/src/test/resources/sql/clean.sql
| @@ -10,3 +10,4 @@ DELETE FROM "im_face_pack_item"; | @@ -10,3 +10,4 @@ DELETE FROM "im_face_pack_item"; | ||
| 10 | DELETE FROM "im_face_user_item"; | 10 | DELETE FROM "im_face_user_item"; |
| 11 | DELETE FROM "im_rtc_call"; | 11 | DELETE FROM "im_rtc_call"; |
| 12 | DELETE FROM "im_rtc_participant"; | 12 | DELETE FROM "im_rtc_participant"; |
| 13 | +DELETE FROM "system_users"; |
yudao-module-im/src/test/resources/sql/create_tables.sql
| @@ -235,3 +235,27 @@ CREATE TABLE IF NOT EXISTS "im_face_user_item" ( | @@ -235,3 +235,27 @@ CREATE TABLE IF NOT EXISTS "im_face_user_item" ( | ||
| 235 | "tenant_id" bigint NOT NULL DEFAULT 0, | 235 | "tenant_id" bigint NOT NULL DEFAULT 0, |
| 236 | PRIMARY KEY ("id") | 236 | PRIMARY KEY ("id") |
| 237 | ) COMMENT 'IM 用户私有表情表'; | 237 | ) COMMENT 'IM 用户私有表情表'; |
| 238 | + | ||
| 239 | +CREATE TABLE IF NOT EXISTS "system_users" ( | ||
| 240 | + "id" bigint NOT NULL GENERATED BY DEFAULT AS IDENTITY, | ||
| 241 | + "username" varchar(30) NOT NULL DEFAULT '', | ||
| 242 | + "password" varchar(100) NOT NULL DEFAULT '', | ||
| 243 | + "nickname" varchar(30) NOT NULL DEFAULT '', | ||
| 244 | + "remark" varchar(500) DEFAULT NULL, | ||
| 245 | + "dept_id" bigint DEFAULT NULL, | ||
| 246 | + "post_ids" varchar(255) DEFAULT NULL, | ||
| 247 | + "email" varchar(50) DEFAULT '', | ||
| 248 | + "mobile" varchar(11) DEFAULT '', | ||
| 249 | + "sex" tinyint DEFAULT 0, | ||
| 250 | + "avatar" varchar(100) DEFAULT '', | ||
| 251 | + "status" tinyint NOT NULL DEFAULT 0, | ||
| 252 | + "login_ip" varchar(50) DEFAULT '', | ||
| 253 | + "login_date" timestamp DEFAULT NULL, | ||
| 254 | + "creator" varchar(64) DEFAULT '', | ||
| 255 | + "create_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 256 | + "updater" varchar(64) DEFAULT '', | ||
| 257 | + "update_time" timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP, | ||
| 258 | + "deleted" bit NOT NULL DEFAULT FALSE, | ||
| 259 | + "tenant_id" bigint NOT NULL DEFAULT 0, | ||
| 260 | + PRIMARY KEY ("id") | ||
| 261 | +) COMMENT '用户信息表'; |