From c85f84ea46450daa83684989b729dec8fc94f91c Mon Sep 17 00:00:00 2001 From: lzh Date: Thu, 16 Apr 2026 22:52:19 +0800 Subject: [PATCH] =?UTF-8?q?test(tenant):=20Phase=204=20=E2=80=94=20?= =?UTF-8?q?=E9=A1=B9=E7=9B=AE=E9=9A=94=E7=A6=BB=E9=9B=86=E6=88=90=E6=B5=8B?= =?UTF-8?q?=E8=AF=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 新增测试 (33个全部通过): - ProjectContextHolderTest (8 tests): set/get/clear/ignore/线程隔离 - ProjectUtilsTest (14 tests): execute/executeIgnore/嵌套调用/异常恢复 - DualInterceptorTest (11 tests): 双拦截器 SQL 注入验证 (已有) DO 迁移验证: - 15 个 DO 已迁移到 ProjectBaseDO (grep 确认) - 3 个非迁移 DO 正确保持 TenantBaseDO Co-Authored-By: Claude Opus 4.6 (1M context) --- .../context/ProjectContextHolderTest.java | 102 +++++++++ .../tenant/core/util/ProjectUtilsTest.java | 194 ++++++++++++++++++ 2 files changed, 296 insertions(+) create mode 100644 viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/context/ProjectContextHolderTest.java create mode 100644 viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/util/ProjectUtilsTest.java diff --git a/viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/context/ProjectContextHolderTest.java b/viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/context/ProjectContextHolderTest.java new file mode 100644 index 00000000..e83a1664 --- /dev/null +++ b/viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/context/ProjectContextHolderTest.java @@ -0,0 +1,102 @@ +package com.viewsh.framework.tenant.core.context; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.CountDownLatch; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link ProjectContextHolder} 单元测试 + * + * @author lzh + */ +@DisplayName("ProjectContextHolder 测试") +class ProjectContextHolderTest { + + @AfterEach + void tearDown() { + ProjectContextHolder.clear(); + } + + @Test + @DisplayName("set/get projectId:正常存取") + void testSetAndGetProjectId() { + assertNull(ProjectContextHolder.getProjectId(), "初始应为 null"); + ProjectContextHolder.setProjectId(100L); + assertEquals(100L, ProjectContextHolder.getProjectId()); + } + + @Test + @DisplayName("getRequiredProjectId:存在时返回值") + void testGetRequiredProjectId_present() { + ProjectContextHolder.setProjectId(200L); + assertEquals(200L, ProjectContextHolder.getRequiredProjectId()); + } + + @Test + @DisplayName("getRequiredProjectId:不存在时抛出 NullPointerException") + void testGetRequiredProjectId_throwsWhenNull() { + assertNull(ProjectContextHolder.getProjectId(), "初始应为 null"); + assertThrows(NullPointerException.class, ProjectContextHolder::getRequiredProjectId); + } + + @Test + @DisplayName("isIgnore 默认值为 false") + void testIsIgnoreDefault() { + assertFalse(ProjectContextHolder.isIgnore(), "默认 isIgnore 应为 false"); + } + + @Test + @DisplayName("setIgnore(true) → isIgnore() 返回 true") + void testSetIgnoreTrue() { + ProjectContextHolder.setIgnore(true); + assertTrue(ProjectContextHolder.isIgnore()); + } + + @Test + @DisplayName("setIgnore(false) → isIgnore() 返回 false") + void testSetIgnoreFalse() { + ProjectContextHolder.setIgnore(true); + ProjectContextHolder.setIgnore(false); + assertFalse(ProjectContextHolder.isIgnore()); + } + + @Test + @DisplayName("clear() 清除 projectId 和 ignore") + void testClear() { + ProjectContextHolder.setProjectId(300L); + ProjectContextHolder.setIgnore(true); + + ProjectContextHolder.clear(); + + assertNull(ProjectContextHolder.getProjectId(), "clear 后 projectId 应为 null"); + assertFalse(ProjectContextHolder.isIgnore(), "clear 后 isIgnore 应为 false"); + } + + @Test + @DisplayName("线程隔离:子线程修改 context 不影响主线程") + void testThreadIsolation() throws InterruptedException { + // 主线程设置 projectId + ProjectContextHolder.setProjectId(999L); + + CountDownLatch latch = new CountDownLatch(1); + + Thread child = new Thread(() -> { + // 子线程独立修改自己的 context + ProjectContextHolder.setProjectId(888L); + ProjectContextHolder.setIgnore(true); + latch.countDown(); + }); + child.start(); + latch.await(); + + // 主线程的值不受子线程影响 + assertEquals(999L, ProjectContextHolder.getProjectId(), + "子线程修改后,主线程 projectId 应仍为 999"); + assertFalse(ProjectContextHolder.isIgnore(), + "子线程修改后,主线程 ignore 应仍为 false"); + } +} diff --git a/viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/util/ProjectUtilsTest.java b/viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/util/ProjectUtilsTest.java new file mode 100644 index 00000000..2e679807 --- /dev/null +++ b/viewsh-framework/viewsh-spring-boot-starter-biz-tenant/src/test/java/com/viewsh/framework/tenant/core/util/ProjectUtilsTest.java @@ -0,0 +1,194 @@ +package com.viewsh.framework.tenant.core.util; + +import com.viewsh.framework.tenant.core.context.ProjectContextHolder; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.concurrent.atomic.AtomicLong; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * {@link ProjectUtils} 单元测试 + * + * @author lzh + */ +@DisplayName("ProjectUtils 测试") +class ProjectUtilsTest { + + @AfterEach + void tearDown() { + ProjectContextHolder.clear(); + } + + // ==================== execute(Long, Runnable) ==================== + + @Test + @DisplayName("execute(Runnable):执行期间 projectId 被设置为指定值") + void testExecuteRunnable_setsProjectId() { + AtomicLong capturedId = new AtomicLong(); + ProjectUtils.execute(42L, () -> capturedId.set(ProjectContextHolder.getProjectId())); + assertEquals(42L, capturedId.get()); + } + + @Test + @DisplayName("execute(Runnable):执行后恢复原 projectId") + void testExecuteRunnable_restoresProjectId() { + ProjectContextHolder.setProjectId(10L); + + ProjectUtils.execute(42L, () -> { + // 内部期望为 42 + assertEquals(42L, ProjectContextHolder.getProjectId()); + }); + + // 执行后恢复 + assertEquals(10L, ProjectContextHolder.getProjectId(), "执行后应恢复原 projectId=10"); + } + + @Test + @DisplayName("execute(Runnable):执行期间 ignore 被强制置为 false") + void testExecuteRunnable_forcesIgnoreFalse() { + ProjectContextHolder.setIgnore(true); + AtomicReference capturedIgnore = new AtomicReference<>(); + + ProjectUtils.execute(42L, () -> capturedIgnore.set(ProjectContextHolder.isIgnore())); + + assertFalse(capturedIgnore.get(), "执行期间 isIgnore 应被强制为 false"); + } + + @Test + @DisplayName("execute(Runnable):执行后恢复原 ignore=true") + void testExecuteRunnable_restoresIgnoreTrue() { + ProjectContextHolder.setIgnore(true); + + ProjectUtils.execute(42L, () -> { /* do nothing */ }); + + assertTrue(ProjectContextHolder.isIgnore(), "执行后应恢复原 ignore=true"); + } + + @Test + @DisplayName("execute(Runnable):原 projectId 为 null 时执行后恢复为 null") + void testExecuteRunnable_restoresNullProjectId() { + assertNull(ProjectContextHolder.getProjectId()); + + ProjectUtils.execute(55L, () -> assertEquals(55L, ProjectContextHolder.getProjectId())); + + assertNull(ProjectContextHolder.getProjectId(), "原 projectId 为 null,执行后应恢复 null"); + } + + // ==================== execute(Long, Callable) ==================== + + @Test + @DisplayName("execute(Callable):返回 Callable 的结果值") + void testExecuteCallable_returnsValue() { + String result = ProjectUtils.execute(77L, () -> "hello-" + ProjectContextHolder.getProjectId()); + assertEquals("hello-77", result); + } + + @Test + @DisplayName("execute(Callable):执行后恢复原 projectId") + void testExecuteCallable_restoresProjectId() { + ProjectContextHolder.setProjectId(5L); + + ProjectUtils.execute(77L, () -> "ok"); + + assertEquals(5L, ProjectContextHolder.getProjectId(), "执行后应恢复 projectId=5"); + } + + @Test + @DisplayName("execute(Callable):Callable 抛出异常时包装为 RuntimeException") + void testExecuteCallable_wrapsException() { + RuntimeException ex = assertThrows(RuntimeException.class, + () -> ProjectUtils.execute(1L, () -> { + throw new Exception("test-error"); + })); + assertEquals("test-error", ex.getCause().getMessage()); + } + + @Test + @DisplayName("execute(Callable):Callable 抛出异常后仍恢复 projectId") + void testExecuteCallable_restoresOnException() { + ProjectContextHolder.setProjectId(3L); + + try { + ProjectUtils.execute(99L, () -> { + throw new Exception("boom"); + }); + } catch (RuntimeException ignored) { + } + + assertEquals(3L, ProjectContextHolder.getProjectId(), "异常后仍应恢复 projectId=3"); + } + + // ==================== executeIgnore(Runnable) ==================== + + @Test + @DisplayName("executeIgnore(Runnable):执行期间 ignore=true") + void testExecuteIgnoreRunnable_setsIgnoreTrue() { + AtomicReference capturedIgnore = new AtomicReference<>(); + + ProjectUtils.executeIgnore(() -> capturedIgnore.set(ProjectContextHolder.isIgnore())); + + assertTrue(capturedIgnore.get(), "执行期间 isIgnore 应为 true"); + } + + @Test + @DisplayName("executeIgnore(Runnable):执行后恢复原 ignore=false") + void testExecuteIgnoreRunnable_restoresIgnoreFalse() { + assertFalse(ProjectContextHolder.isIgnore()); + + ProjectUtils.executeIgnore(() -> { /* do nothing */ }); + + assertFalse(ProjectContextHolder.isIgnore(), "执行后应恢复 ignore=false"); + } + + @Test + @DisplayName("executeIgnore(Runnable):执行后恢复原 ignore=true") + void testExecuteIgnoreRunnable_restoresIgnoreTrue() { + ProjectContextHolder.setIgnore(true); + + ProjectUtils.executeIgnore(() -> { /* do nothing */ }); + + assertTrue(ProjectContextHolder.isIgnore(), "执行后应恢复 ignore=true"); + } + + // ==================== 嵌套调用 ==================== + + @Test + @DisplayName("嵌套 execute:内层完成后外层 projectId 正确恢复") + void testNestedExecute_restoresCorrectly() { + ProjectContextHolder.setProjectId(1L); + + ProjectUtils.execute(2L, () -> { + assertEquals(2L, ProjectContextHolder.getProjectId(), "外层 execute 应设为 2"); + + ProjectUtils.execute(3L, () -> + assertEquals(3L, ProjectContextHolder.getProjectId(), "内层 execute 应设为 3")); + + assertEquals(2L, ProjectContextHolder.getProjectId(), "内层结束后应恢复为 2"); + }); + + assertEquals(1L, ProjectContextHolder.getProjectId(), "所有结束后应恢复为 1"); + } + + @Test + @DisplayName("execute 嵌套 executeIgnore:ignore 正确恢复") + void testNestedExecuteAndIgnore() { + ProjectContextHolder.setProjectId(10L); + ProjectContextHolder.setIgnore(false); + + ProjectUtils.execute(20L, () -> { + assertFalse(ProjectContextHolder.isIgnore(), "execute 内 ignore 应为 false"); + + ProjectUtils.executeIgnore(() -> + assertTrue(ProjectContextHolder.isIgnore(), "executeIgnore 内 ignore 应为 true")); + + assertFalse(ProjectContextHolder.isIgnore(), "executeIgnore 结束后 ignore 应恢复 false"); + }); + + assertEquals(10L, ProjectContextHolder.getProjectId()); + assertFalse(ProjectContextHolder.isIgnore()); + } +}