|
|
@@ -0,0 +1,118 @@
|
|
|
+package com.zsjz.ai.common.datasource;
|
|
|
+
|
|
|
+import cn.dev33.satoken.stp.StpUtil;
|
|
|
+import org.junit.jupiter.api.DisplayName;
|
|
|
+import org.junit.jupiter.api.Test;
|
|
|
+import org.mockito.MockedStatic;
|
|
|
+
|
|
|
+import java.util.List;
|
|
|
+import java.util.Set;
|
|
|
+import java.util.concurrent.atomic.AtomicBoolean;
|
|
|
+
|
|
|
+import static org.junit.jupiter.api.Assertions.assertFalse;
|
|
|
+import static org.junit.jupiter.api.Assertions.assertTrue;
|
|
|
+import static org.mockito.Mockito.mock;
|
|
|
+import static org.mockito.Mockito.mockStatic;
|
|
|
+import static org.mockito.Mockito.never;
|
|
|
+import static org.mockito.Mockito.verify;
|
|
|
+import static org.mockito.Mockito.when;
|
|
|
+
|
|
|
+/**
|
|
|
+ * 兜底回收器的判定契约。
|
|
|
+ *
|
|
|
+ * <p>守两件事:
|
|
|
+ * <ol>
|
|
|
+ * <li><b>按「打开案件的 token」判活</b>:被顶下线后用户往往马上重新登录,
|
|
|
+ * 按用户维度判断会把旧案件永远留在内存里;</li>
|
|
|
+ * <li><b>在途任务优先</b>:会话结束了但清洗/治理还在跑,本轮不关数据源。
|
|
|
+ * 任务结束后下一轮必须能接着关(这正是监听器跳过时承诺的「交给 Reaper 兜底」)。</li>
|
|
|
+ * </ol>
|
|
|
+ */
|
|
|
+class CaseDataSourceReaperTest {
|
|
|
+
|
|
|
+ private static final Long CASE_ID = 27L;
|
|
|
+ private static final Long USER_ID = 1L;
|
|
|
+ private static final String OWNER_TOKEN = "owner-token";
|
|
|
+
|
|
|
+ private final CaseDataSourceRegistry registry = mock(CaseDataSourceRegistry.class);
|
|
|
+
|
|
|
+ private void stubOpenedCase() {
|
|
|
+ when(registry.openedCount()).thenReturn(1);
|
|
|
+ when(registry.openedCaseIds()).thenReturn(Set.of(CASE_ID));
|
|
|
+ when(registry.ownerOf(CASE_ID)).thenReturn(USER_ID);
|
|
|
+ when(registry.ownerTokenOf(CASE_ID)).thenReturn(OWNER_TOKEN);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 用可变的「会话是否有效 / 是否有在途任务」驱动 reap,避免依赖真实 sa-token 上下文 */
|
|
|
+ private CaseDataSourceReaper reaper(AtomicBoolean sessionAlive, AtomicBoolean workRunning) {
|
|
|
+ stubOpenedCase();
|
|
|
+ CaseActivityProbe probe = caseId -> workRunning.get();
|
|
|
+ return new CaseDataSourceReaper(registry, List.of(probe)) {
|
|
|
+ @Override
|
|
|
+ boolean isOwnerSessionAlive(Long ownerUserId, Long caseId) {
|
|
|
+ return sessionAlive.get();
|
|
|
+ }
|
|
|
+ };
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("归属会话有效:保留案件数据源")
|
|
|
+ void keepsCaseWhenOwnerSessionAlive() {
|
|
|
+ CaseDataSourceReaper reaper = reaper(new AtomicBoolean(true), new AtomicBoolean(false));
|
|
|
+
|
|
|
+ reaper.reap();
|
|
|
+
|
|
|
+ verify(registry, never()).close(CASE_ID);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("归属会话已结束:回收案件数据源")
|
|
|
+ void reapsCaseWhenOwnerSessionEnded() {
|
|
|
+ CaseDataSourceReaper reaper = reaper(new AtomicBoolean(false), new AtomicBoolean(false));
|
|
|
+
|
|
|
+ reaper.reap();
|
|
|
+
|
|
|
+ verify(registry).close(CASE_ID);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("会话结束但在途任务未完成:本轮跳过,任务结束后下一轮回收")
|
|
|
+ void skipsCaseWithRunningWorkThenReaps() {
|
|
|
+ AtomicBoolean workRunning = new AtomicBoolean(true);
|
|
|
+ CaseDataSourceReaper reaper = reaper(new AtomicBoolean(false), workRunning);
|
|
|
+
|
|
|
+ reaper.reap();
|
|
|
+ verify(registry, never()).close(CASE_ID);
|
|
|
+
|
|
|
+ workRunning.set(false);
|
|
|
+ reaper.reap();
|
|
|
+ verify(registry).close(CASE_ID);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("ownerToken 已被顶下线:判定会话已结束(即使账号还有别的有效 token)")
|
|
|
+ void replacedOwnerTokenIsNotAlive() {
|
|
|
+ stubOpenedCase();
|
|
|
+ CaseDataSourceReaper reaper = new CaseDataSourceReaper(registry, List.of());
|
|
|
+
|
|
|
+ try (MockedStatic<StpUtil> stp = mockStatic(StpUtil.class)) {
|
|
|
+ // sa-token 对「被顶下线/被踢/过期」的 token 返回 null(映射里写的是异常标记)
|
|
|
+ stp.when(() -> StpUtil.getLoginIdByToken(OWNER_TOKEN)).thenReturn(null);
|
|
|
+
|
|
|
+ assertFalse(reaper.isOwnerSessionAlive(USER_ID, CASE_ID));
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ @Test
|
|
|
+ @DisplayName("ownerToken 仍有效:保留案件数据源")
|
|
|
+ void validOwnerTokenIsAlive() {
|
|
|
+ stubOpenedCase();
|
|
|
+ CaseDataSourceReaper reaper = new CaseDataSourceReaper(registry, List.of());
|
|
|
+
|
|
|
+ try (MockedStatic<StpUtil> stp = mockStatic(StpUtil.class)) {
|
|
|
+ stp.when(() -> StpUtil.getLoginIdByToken(OWNER_TOKEN)).thenReturn(String.valueOf(USER_ID));
|
|
|
+
|
|
|
+ assertTrue(reaper.isOwnerSessionAlive(USER_ID, CASE_ID));
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|