Commit 16d5c0b0cc5dba7d899f94700543daf1de5bdddd

Authored by YunaiV
1 parent 843cc90b

✨ feat(im): 完善单元测试

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