mirror of
https://gitee.com/yudaocode/yudao-boot-mini.git
synced 2026-10-06 06:12:58 +08:00
fix(im): 修复关闭多租户后敏感词检测报错的问题
This commit is contained in:
+23
-6
@@ -22,6 +22,7 @@ import jakarta.annotation.Resource;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
|
||||
@@ -48,6 +49,14 @@ import static cn.iocoder.yudao.module.im.enums.ErrorCodeConstants.SENSITIVE_WORD
|
||||
@Slf4j
|
||||
public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
|
||||
/**
|
||||
* 关闭多租户时的统一缓存键,仅用于缓存,不作为查询的租户编号
|
||||
*/
|
||||
private static final Long SINGLE_TENANT_CACHE_KEY = 0L;
|
||||
|
||||
@Value("${yudao.tenant.enable:true}")
|
||||
private boolean tenantEnable = true;
|
||||
|
||||
@Resource
|
||||
private ImSensitiveWordMapper sensitiveWordMapper;
|
||||
|
||||
@@ -70,7 +79,7 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
}
|
||||
|
||||
/**
|
||||
* 租户 → SensitiveWordBs 实例的本地缓存
|
||||
* 租户 → SensitiveWordBs 实例的本地缓存;关闭多租户时共用一个缓存
|
||||
* <p>
|
||||
* 每分钟触发一次异步 reload,先读 max(update_time),没变就复用旧实例(避免 trie 重建),变了才重新读词库 + 重建。
|
||||
* 单实例 CRUD 后另外通过 {@link #invalidateSensitiveWordBsCaches()} 立即让本机失效,多实例靠定时刷新最长 1 分钟内收敛。
|
||||
@@ -87,8 +96,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
|
||||
@Override
|
||||
public ListenableFuture<SensitiveWordBsCache> reload(Long tenantId, SensitiveWordBsCache oldValue) {
|
||||
// 异步刷新线程独立于业务线程,没有租户上下文;必须显式 TenantUtils.execute 设置,否则租户拦截器会按当前线程的空上下文拼 SQL
|
||||
return Futures.immediateFuture(TenantUtils.execute(tenantId, () -> {
|
||||
// 异步刷新时恢复租户上下文;关闭多租户时不设置租户编号
|
||||
return Futures.immediateFuture(TenantUtils.execute(tenantEnable ? tenantId : null, () -> {
|
||||
LocalDateTime currentMax = sensitiveWordMapper.selectMaxUpdateTime(tenantId);
|
||||
// 没变 → 复用旧实例,避免无谓地重建 trie
|
||||
if (Objects.equals(oldValue.getMaxUpdateTime(), currentMax)) {
|
||||
@@ -102,7 +111,7 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
});
|
||||
|
||||
private SensitiveWordBsCache loadFresh(Long tenantId) {
|
||||
return TenantUtils.execute(tenantId, () -> {
|
||||
return TenantUtils.execute(tenantEnable ? tenantId : null, () -> {
|
||||
// 先取基线时间再读词库:反过来在两次查询之间出现的新插入会被漏感知
|
||||
LocalDateTime maxUpdateTime = sensitiveWordMapper.selectMaxUpdateTime(tenantId);
|
||||
List<ImSensitiveWordDO> words = sensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
@@ -122,9 +131,16 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
/**
|
||||
* 强制让敏感词缓存失效,下次访问按最新 DB 重建
|
||||
* <p>
|
||||
* 有租户上下文:仅失效该租户。无租户上下文(如系统级 / 跨租户清理):兜底失效所有租户。
|
||||
* 关闭多租户时失效统一缓存;开启时仅失效当前租户,无租户上下文则失效所有租户。
|
||||
*/
|
||||
private void invalidateSensitiveWordBsCaches() {
|
||||
// 情况一:租户未开启的清理
|
||||
if (!tenantEnable) {
|
||||
sensitiveWordBsCaches.invalidate(SINGLE_TENANT_CACHE_KEY);
|
||||
return;
|
||||
}
|
||||
|
||||
// 情况二:租户已开启的清理
|
||||
Long tenantId = TenantContextHolder.getTenantId();
|
||||
if (tenantId != null) {
|
||||
sensitiveWordBsCaches.invalidate(tenantId);
|
||||
@@ -138,7 +154,8 @@ public class ImSensitiveWordServiceImpl implements ImSensitiveWordService {
|
||||
if (StrUtil.isBlank(text)) {
|
||||
return;
|
||||
}
|
||||
SensitiveWordBs bs = sensitiveWordBsCaches.getUnchecked(TenantContextHolder.getRequiredTenantId()).getBs();
|
||||
Long cacheKey = tenantEnable ? TenantContextHolder.getRequiredTenantId() : SINGLE_TENANT_CACHE_KEY;
|
||||
SensitiveWordBs bs = sensitiveWordBsCaches.getUnchecked(cacheKey).getBs();
|
||||
if (bs.contains(text)) {
|
||||
throw exception(MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
}
|
||||
|
||||
+160
-65
@@ -2,24 +2,28 @@ 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 com.google.common.cache.LoadingCache;
|
||||
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 org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
import static cn.iocoder.yudao.framework.test.core.util.AssertUtils.assertServiceException;
|
||||
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.awaitility.Awaitility.await;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
import static org.mockito.Mockito.*;
|
||||
|
||||
@@ -40,10 +44,9 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@BeforeEach
|
||||
public void setUp() {
|
||||
// 设置租户上下文;validateText 与 loadFresh 都依赖 TenantContextHolder
|
||||
// 设置租户上下文
|
||||
TenantContextHolder.setTenantId(TENANT_ID);
|
||||
// mock 启用敏感词列表;LoadingCache 在首次 validateText 时懒加载,无需主动 init
|
||||
// 用 lenient 避免 null / empty 等不触发 cache load 的用例报 UnnecessaryStubbing
|
||||
// mock 启用的敏感词,部分用例不触发缓存加载
|
||||
lenient().when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
@@ -55,69 +58,165 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
|
||||
|
||||
@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_tenantDisabled() {
|
||||
// 准备上下文
|
||||
ReflectionTestUtils.setField(sensitiveWordService, "tenantEnable", false);
|
||||
TenantContextHolder.clear();
|
||||
|
||||
// 调用,并断言
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("hello world"));
|
||||
assertServiceException(() -> sensitiveWordService.validateText("this contains badword here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_tenantEnabledWithoutTenantId() {
|
||||
// 准备上下文
|
||||
TenantContextHolder.clear();
|
||||
|
||||
// 调用,并断言异常
|
||||
assertThrows(NullPointerException.class, () -> sensitiveWordService.validateText("hello world"));
|
||||
// 断言
|
||||
verifyNoInteractions(imSensitiveWordMapper);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_tenantIsolation() {
|
||||
// mock 不同租户的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenAnswer(invocation -> List.of(ImSensitiveWordDO.builder()
|
||||
.word(TenantContextHolder.getRequiredTenantId().equals(TENANT_ID) ? "firstbad" : "secondbad")
|
||||
.build()));
|
||||
|
||||
// 调用,并断言当前租户的词库
|
||||
assertServiceException(() -> sensitiveWordService.validateText("firstbad"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("secondbad"));
|
||||
// 调用,并断言切换租户后的词库
|
||||
TenantContextHolder.setTenantId(2L);
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("firstbad"));
|
||||
assertServiceException(() -> sensitiveWordService.validateText("secondbad"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
// 调用,并断言切回租户后复用缓存
|
||||
TenantContextHolder.setTenantId(TENANT_ID);
|
||||
assertServiceException(() -> sensitiveWordService.validateText("firstbad"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWordList_tenantDisabled() {
|
||||
// 准备上下文
|
||||
ReflectionTestUtils.setField(sensitiveWordService, "tenantEnable", false);
|
||||
TenantContextHolder.clear();
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
|
||||
// 调用,并断言携带租户编号时仍复用缓存
|
||||
TenantContextHolder.setTenantId(2L);
|
||||
sensitiveWordService.validateText("hello world");
|
||||
verify(imSensitiveWordMapper).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
// mock 删除后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus())).thenReturn(List.of());
|
||||
|
||||
// 调用
|
||||
sensitiveWordService.deleteSensitiveWordList(ListUtil.of(1L, 2L));
|
||||
|
||||
// 断言缓存已刷新,上下文不变
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("badword"));
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
assertEquals(2L, TenantContextHolder.getTenantId());
|
||||
}
|
||||
|
||||
@Test
|
||||
@SuppressWarnings("unchecked")
|
||||
public void testValidateText_tenantDisabledRefresh() {
|
||||
// 准备上下文
|
||||
ReflectionTestUtils.setField(sensitiveWordService, "tenantEnable", false);
|
||||
TenantContextHolder.clear();
|
||||
// mock 初始更新时间
|
||||
when(imSensitiveWordMapper.selectMaxUpdateTime(0L)).thenReturn(LocalDateTime.of(2026, 9, 1, 0, 0));
|
||||
// 准备缓存
|
||||
sensitiveWordService.validateText("hello world");
|
||||
// mock 词库更新,并断言刷新线程无租户编号
|
||||
when(imSensitiveWordMapper.selectMaxUpdateTime(0L)).thenAnswer(invocation -> {
|
||||
assertNull(TenantContextHolder.getTenantId());
|
||||
return LocalDateTime.of(2026, 9, 2, 0, 0);
|
||||
});
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(List.of(ImSensitiveWordDO.builder().word("newbad").build()));
|
||||
|
||||
// 调用
|
||||
LoadingCache<Long, ?> cache = (LoadingCache<Long, ?>) ReflectionTestUtils.getField(
|
||||
sensitiveWordService, "sensitiveWordBsCaches");
|
||||
cache.refresh(0L);
|
||||
|
||||
// 断言缓存异步刷新
|
||||
await().atMost(Duration.ofSeconds(5)).untilAsserted(() ->
|
||||
assertServiceException(() -> sensitiveWordService.validateText("newbad"), MESSAGE_SENSITIVE_WORD_BLOCKED));
|
||||
assertDoesNotThrow(() -> sensitiveWordService.validateText("badword"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_hitEnglish() {
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("this contains badword here"));
|
||||
assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.validateText("this contains badword here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testValidateText_hitChinese() {
|
||||
ServiceException exception = assertThrows(ServiceException.class,
|
||||
() -> sensitiveWordService.validateText("这条消息里有违禁词哦"));
|
||||
assertEquals(MESSAGE_SENSITIVE_WORD_BLOCKED.getCode(), exception.getCode());
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
}
|
||||
|
||||
@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 走通
|
||||
// mock 新增后的词库
|
||||
when(imSensitiveWordMapper.selectListByStatus(CommonStatusEnum.ENABLE.getStatus()))
|
||||
.thenReturn(ListUtil.of(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
@@ -129,149 +228,145 @@ public class ImSensitiveWordServiceImplTest extends BaseMockitoUnitTest {
|
||||
));
|
||||
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"));
|
||||
// 断言缓存已刷新
|
||||
assertServiceException(() -> sensitiveWordService.validateText("contains newbad here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
// 断言原有敏感词仍然生效
|
||||
assertServiceException(() -> sensitiveWordService.validateText("contains badword here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
|
||||
// 断言:selectListByStatus 共被调用 2 次(首次 load + invalidate 后 reload)
|
||||
// 断言词库重新加载
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCreateSensitiveWord_duplicateWord_throws() {
|
||||
// 准备:mock 已存在同名敏感词
|
||||
// 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
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.createSensitiveWord(reqVO), SENSITIVE_WORD_DUPLICATED, reqVO.getWord());
|
||||
|
||||
// 断言
|
||||
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 校验通过
|
||||
// mock 原敏感词
|
||||
when(imSensitiveWordMapper.selectById(1L)).thenReturn(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build());
|
||||
when(imSensitiveWordMapper.selectByWord("updatedbad")).thenReturn(null);
|
||||
// 准备:reload 时返回新词库
|
||||
// mock 修改后的词库
|
||||
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());
|
||||
// 断言缓存已刷新
|
||||
assertServiceException(() -> sensitiveWordService.validateText("contains updatedbad here"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
|
||||
// 断言: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 校验通过
|
||||
// mock 原敏感词
|
||||
when(imSensitiveWordMapper.selectById(1L)).thenReturn(
|
||||
ImSensitiveWordDO.builder().id(1L).word("badword")
|
||||
.status(CommonStatusEnum.ENABLE.getStatus()).build());
|
||||
// 准备:reload 时返回剩余词库(只剩中文那条)
|
||||
// mock 删除后的词库
|
||||
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("这条消息里有违禁词哦"));
|
||||
// 断言未删除的敏感词仍然生效
|
||||
assertServiceException(() -> sensitiveWordService.validateText("这条消息里有违禁词哦"), MESSAGE_SENSITIVE_WORD_BLOCKED);
|
||||
|
||||
// 断言:selectListByStatus 共被调用 2 次
|
||||
// 断言词库重新加载
|
||||
verify(imSensitiveWordMapper, times(2)).selectListByStatus(CommonStatusEnum.ENABLE.getStatus());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testDeleteSensitiveWord_notExists_throws() {
|
||||
// 准备:selectById 返回 null
|
||||
// mock 敏感词不存在
|
||||
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());
|
||||
// 调用,并断言异常
|
||||
assertServiceException(() -> sensitiveWordService.deleteSensitiveWord(999L), SENSITIVE_WORD_NOT_EXISTS);
|
||||
|
||||
// 断言:未走到 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 时返回空词库
|
||||
// mock 删除后的词库
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user