liyong
10 小时以前 7a23c450f3ac85de7dca1b908de273ff636ce218
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
package cn.iocoder.yudao.module.im.service.sensitiveword;
 
import cn.hutool.core.collection.ListUtil;
import cn.iocoder.yudao.framework.common.enums.CommonStatusEnum;
import cn.iocoder.yudao.framework.common.exception.ServiceException;
import cn.iocoder.yudao.framework.tenant.core.context.TenantContextHolder;
import cn.iocoder.yudao.framework.test.core.ut.BaseMockitoUnitTest;
import cn.iocoder.yudao.module.im.controller.admin.manager.sensitiveword.vo.ImSensitiveWordSaveReqVO;
import cn.iocoder.yudao.module.im.dal.dataobject.sensitiveword.ImSensitiveWordDO;
import cn.iocoder.yudao.module.im.dal.mysql.sensitiveword.ImSensitiveWordMapper;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
 
import java.time.LocalDateTime;
import java.util.List;
 
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.MESSAGE_SENSITIVE_WORD_BLOCKED;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_DUPLICATED;
import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD_NOT_EXISTS;
import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;
 
/**
 * {@link ImSensitiveWordServiceImpl} 的单元测试
 *
 * @author 芋道源码
 */
public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
 
    private static final Long TENANT_ID = 1L;
 
    @InjectMocks
    private ImSensitiveWordServiceImpl sensitiveWordService;
 
    @Mock
    private ImSensitiveWordMapper imSensitiveWordMapper;
 
    @BeforeEach
    public void setUp() {
        // 设置租户上下文;validateText 与 loadFresh 都依赖 TenantContextHolder
        TenantContextHolder.setTenantId(TENANT_ID);
        // mock 启用敏感词列表;LoadingCache 在首次 validateText 时懒加载,无需主动 init
        // 用 lenient 避免 null / empty 等不触发 cache load 的用例报 UnnecessaryStubbing
        lenient().when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
                .thenReturn(ListUtil.of(
                        ImSensitiveWordDO.builder().id(1L).word("badword")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build(),
                        ImSensitiveWordDO.builder().id(2L).word("违禁词")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build()
                ));
    }
 
    @AfterEach
    public void tearDown() {
        // 清理租户上下文,避免污染其它测试
        TenantContextHolder.clear();
    }
 
    @Test
    public void testValidateText_null() {
        // null 直接返回,不抛异常
        assertDoesNotThrow(() -> sensitiveWordService.validateText(null));
    }
 
    @Test
    public void testValidateText_empty() {
        assertDoesNotThrow(() -> sensitiveWordService.validateText(""));
    }
 
    @Test
    public void testValidateText_clean() {
        // 正常文本不应命中
        assertDoesNotThrow(() -> sensitiveWordService.validateText("hello world"));
    }
 
    @Test
    public void testValidateText_hitEnglish() {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> sensitiveWordService.validateText("this contains badword here"));
        assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
    }
 
    @Test
    public void testValidateText_hitChinese() {
        ServiceException exception = assertThrows(ServiceException.class,
                () -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
        assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
    }
 
    @Test
    public void testValidateText_lazyLoadsCacheOnFirstCall() {
        // 调用:首次 validateText 应触发 cache load
        sensitiveWordService.validateText("hello world");
 
        // 断言:mapper 各调用 1 次(loadFresh 里先取 maxUpdateTime 再读词库)
        verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID);
        verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Test
    public void testValidateText_reusesCachedBsAcrossCalls() {
        // 调用:连续两次 validateText
        sensitiveWordService.validateText("hello world");
        sensitiveWordService.validateText("another text");
 
        // 断言:第二次复用 cache,mapper 仍只被调用 1 次
        verify(imSensitiveWordMapper, times(1)).selectMaxUpdateTime(TENANT_ID);
        verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Test
    public void testCreateSensitiveWord_invalidatesCacheAndReloadsOnNextValidate() {
        // 准备:首次 validateText 触发 cache load(旧词库)
        sensitiveWordService.validateText("hello world");
        verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
 
        // 准备:mapper 返回新词库(额外多一个 newbad),并让 createSensitiveWord 走通
        when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
                .thenReturn(ListUtil.of(
                        ImSensitiveWordDO.builder().id(1L).word("badword")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build(),
                        ImSensitiveWordDO.builder().id(2L).word("违禁词")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build(),
                        ImSensitiveWordDO.builder().id(3L).word("newbad")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build()
                ));
        when(imSensitiveWordMapper.selectByWord("newbad")).thenReturn(null);
 
        // 调用:新增敏感词,触发 invalidate
        ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
        reqVO.setWord("newbad");
        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
        sensitiveWordService.createSensitiveWord(reqVO);
 
        // 调用:再次 validateText,应触发重新 load
        ServiceException exception = assertThrows(ServiceException.class,
                () -> sensitiveWordService.validateText("contains newbad here"));
        assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
        // 旧词依然命中
        assertThrows(ServiceException.class,
                () -> sensitiveWordService.validateText("contains badword here"));
 
        // 断言:selectListByStatus 共被调用 2 次(首次 load + invalidate 后 reload)
        verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Test
    public void testCreateSensitiveWord_duplicateWord_throws() {
        // 准备:mock 已存在同名敏感词
        when(imSensitiveWordMapper.selectByWord("dup")).thenReturn(
                ImSensitiveWordDO.builder().id(99L).word("dup")
                        .status(CommonStatusEnum.ENABLE.getStatus()).build());
 
        // 调用 + 断言:重复敏感词抛 SENSITIVE_WORD_DUPLICATED
        ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
        reqVO.setWord("dup");
        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
        ServiceException exception = assertThrows(ServiceException.class,
                () -> sensitiveWordService.createSensitiveWord(reqVO));
        assertEquals(SENSITIVE_WORD_DUPLICATED.getCode(), exception.getCode());
 
        // 断言:未走到 insert
        verify(imSensitiveWordMapper, never()).insert(any(ImSensitiveWordDO.class));
    }
 
    @Test
    public void testUpdateSensitiveWord_invalidatesCache() {
        // 准备:首次 validateText 触发 cache load
        sensitiveWordService.validateText("hello world");
        verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
 
        // 准备:让 update 校验通过
        when(imSensitiveWordMapper.selectById(1L)).thenReturn(
                ImSensitiveWordDO.builder().id(1L).word("badword")
                        .status(CommonStatusEnum.ENABLE.getStatus()).build());
        when(imSensitiveWordMapper.selectByWord("updatedbad")).thenReturn(null);
        // 准备:reload 时返回新词库
        when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
                .thenReturn(ListUtil.of(
                        ImSensitiveWordDO.builder().id(1L).word("updatedbad")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build()
                ));
 
        // 调用:更新敏感词,触发 invalidate
        ImSensitiveWordSaveReqVO reqVO = new ImSensitiveWordSaveReqVO();
        reqVO.setId(1L);
        reqVO.setWord("updatedbad");
        reqVO.setStatus(CommonStatusEnum.ENABLE.getStatus());
        sensitiveWordService.updateSensitiveWord(reqVO);
 
        // 调用 + 断言:再次 validateText 应使用新词库
        ServiceException exception = assertThrows(ServiceException.class,
                () -> sensitiveWordService.validateText("contains updatedbad here"));
        assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
 
        // 断言:selectListByStatus 共被调用 2 次
        verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Test
    public void testDeleteSensitiveWord_invalidatesCache() {
        // 准备:首次 validateText 触发 cache load
        sensitiveWordService.validateText("hello world");
        verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
 
        // 准备:让 delete 校验通过
        when(imSensitiveWordMapper.selectById(1L)).thenReturn(
                ImSensitiveWordDO.builder().id(1L).word("badword")
                        .status(CommonStatusEnum.ENABLE.getStatus()).build());
        // 准备:reload 时返回剩余词库(只剩中文那条)
        when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
                .thenReturn(ListUtil.of(
                        ImSensitiveWordDO.builder().id(2L).word("违禁词")
                                .status(CommonStatusEnum.ENABLE.getStatus()).build()
                ));
 
        // 调用:删除 badword,触发 invalidate
        sensitiveWordService.deleteSensitiveWord(1L);
 
        // 调用 + 断言:badword 已被删,不再命中
        assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here"));
        // 中文词依然在
        assertThrows(ServiceException.class,
                () -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
 
        // 断言:selectListByStatus 共被调用 2 次
        verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Test
    public void testDeleteSensitiveWord_notExists_throws() {
        // 准备:selectById 返回 null
        when(imSensitiveWordMapper.selectById(999L)).thenReturn(null);
 
        // 调用 + 断言:抛 SENSITIVE_WORD_NOT_EXISTS
        ServiceException exception = assertThrows(ServiceException.class,
                () -> sensitiveWordService.deleteSensitiveWord(999L));
        assertEquals(SENSITIVE_WORD_NOT_EXISTS.getCode(), exception.getCode());
 
        // 断言:未走到 deleteById
        verify(imSensitiveWordMapper, never()).deleteById(anyLong());
    }
 
    @Test
    public void testDeleteSensitiveWordList_invalidatesCache() {
        // 准备:首次 validateText 触发 cache load
        sensitiveWordService.validateText("hello world");
        verify(imSensitiveWordMapper, times(1)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
 
        // 准备:reload 时返回空词库
        when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
                .thenReturn(ListUtil.of());
 
        // 调用:批量删除,触发 invalidate
        sensitiveWordService.deleteSensitiveWordList(ListUtil.of(1L, 2L));
 
        // 调用 + 断言:所有词都不再命中
        assertDoesNotThrow(() -> sensitiveWordService.validateText("contains badword here"));
        assertDoesNotThrow(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
 
        // 断言:deleteByIds 被调用 1 次;selectListByStatus 共 2 次
        verify(imSensitiveWordMapper, times(1)).deleteByIds(ListUtil.of(1L, 2L));
        verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
    }
 
    @Test
    public void testDeleteSensitiveWordList_emptyIds_skip() {
        // 调用:空列表直接返回
        sensitiveWordService.deleteSensitiveWordList(ListUtil.of());
 
        // 断言:mapper 不被调用
        verify(imSensitiveWordMapper, never()).deleteByIds(anyList());
    }
 
}