From 15aece12251681efa3d56f0035a0fadc6d5bc3c9 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=96=AF=E7=8B=82=E7=9A=84=E7=8B=AE=E5=AD=90Li?=
<15040126243@163.com>
Date: Tue, 15 Sep 2026 20:05:01 +0800
Subject: [PATCH] =?UTF-8?q?add=20=E4=BC=98=E5=8C=96=20=E4=B8=BA=20common?=
=?UTF-8?q?=20=E7=9A=84=E6=89=80=E6=9C=89=E5=8A=9F=E8=83=BD=E5=A2=9E?=
=?UTF-8?q?=E5=8A=A0=E5=8D=95=E5=85=83=E6=B5=8B=E8=AF=95=20=E7=A1=AE?=
=?UTF-8?q?=E4=BF=9D=E4=BB=A5=E5=90=8E=E6=94=B9=E5=8A=A8=E7=9A=84=E5=8A=9F?=
=?UTF-8?q?=E8=83=BD=E4=B8=80=E8=87=B4=E6=80=A7?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
pom.xml | 18 +-
ruoyi-admin/pom.xml | 7 -
.../java/org/dromara/test/AssertUnitTest.java | 58 --
.../java/org/dromara/test/DemoUnitTest.java | 102 ----
.../java/org/dromara/test/ParamUnitTest.java | 106 ----
.../java/org/dromara/test/TagUnitTest.java | 72 ---
.../dromara/common/ai/SnailAiConfigTest.java | 28 +
.../dromara/common/core/utils/DateUtils.java | 2 +-
.../common/core/utils/sql/SqlUtil.java | 4 +-
.../common/core/domain/ResponseModelTest.java | 72 +++
.../core/enums/BusinessStatusEnumTest.java | 77 +++
.../factory/YmlPropertySourceFactoryTest.java | 67 ++
.../CoreFrameworkUtilityContractTest.java | 251 ++++++++
.../utils/CoreInfrastructureContractTest.java | 133 ++++
.../core/utils/CoreUtilityBoundaryTest.java | 156 +++++
.../utils/DateNetDesensitizedUtilsTest.java | 114 ++++
.../common/core/utils/SqlUtilTest.java | 54 ++
.../common/core/utils/StreamUtilsTest.java | 102 ++++
.../common/core/utils/StringUtilsTest.java | 100 +++
.../common/core/utils/TreeBuildUtilsTest.java | 98 +++
.../enums/EnumPatternValidatorTest.java | 85 +++
.../doc/SaTokenSecurityMetadataTest.java | 45 ++
.../ActuatorEnvironmentPostProcessorTest.java | 48 ++
.../common/encrypt/EncryptFunctionTest.java | 111 ++++
.../encrypt/EncryptInfrastructureTest.java | 156 +++++
.../common/excel/ExcelBuilderTest.java | 326 ++++++++++
.../common/excel/ExcelConvertTest.java | 146 +++++
.../common/excel/ExcelFunctionTest.java | 82 +++
.../common/excel/ExcelWriterWrapperTest.java | 94 +++
.../common/job/SnailJobConfigTest.java | 30 +
.../dromara/common/json/utils/JsonUtils.java | 3 +-
.../dromara/common/json/JsonTestContext.java | 37 ++
.../common/json/config/JacksonConfigTest.java | 56 ++
.../enhance/JsonEnhancementContextTest.java | 46 ++
.../json/enhance/JsonValueEnhancerTest.java | 174 ++++++
.../common/json/utils/JsonUtilsTest.java | 108 ++++
.../validate/JsonPatternValidatorTest.java | 63 ++
.../liteflow/LiteFlowComponentTest.java | 73 +++
.../common/liteflow/LiteFlowUtilsTest.java | 127 ++++
.../dromara/common/log/LogFunctionTest.java | 184 ++++++
.../common/mail/MailPropertiesTest.java | 46 ++
.../org/dromara/common/mcp/McpResultTest.java | 67 ++
.../mqtt/MqttAutoConfigurationTest.java | 40 ++
.../core/query/LambdaJoinQueryBuilder.java | 6 +-
.../mapper/LambdaCrudChainWrapperTest.java | 253 ++++++++
.../mybatis/core/page/PageQueryTest.java | 91 +++
.../core/query/AggregateSelectUtilsTest.java | 65 ++
.../query/LambdaJoinQueryBuilderTest.java | 312 ++++++++++
.../core/query/LambdaQueryBuilderTest.java | 418 +++++++++++++
.../helper/DataPermissionHelperTest.java | 85 +++
.../helper/MybatisUtilityContractTest.java | 187 ++++++
.../common/oss/util/BucketUrlUtil.java | 8 +-
.../dromara/common/oss/OssFunctionTest.java | 129 ++++
.../dromara/common/push/PushFunctionTest.java | 55 ++
.../common/redis/RedisAspectContractTest.java | 576 ++++++++++++++++++
.../common/redis/RedisConfigurationTest.java | 66 ++
.../common/redis/RedisFunctionTest.java | 40 ++
.../manager/CaffeineCacheDecoratorTest.java | 77 +++
.../manager/PlusSpringCacheManagerTest.java | 57 ++
.../common/satoken/SaTokenFunctionTest.java | 139 +++++
.../common/security/SecurityConfigTest.java | 118 ++++
.../sensitive/SensitiveStrategyTest.java | 93 +++
.../common/sms/SmsExceptionHandlerTest.java | 30 +
.../social/SocialConfigurationTest.java | 47 ++
.../common/social/SocialUtilsTest.java | 155 +++++
.../core/TranslationInterfaceTest.java | 55 ++
.../TranslationJsonFieldProcessorTest.java | 207 +++++++
.../impl/TranslationImplementationsTest.java | 148 +++++
.../dromara/common/web/WebFunctionTest.java | 136 +++++
.../common/web/WebInfrastructureTest.java | 227 +++++++
70 files changed, 7193 insertions(+), 355 deletions(-)
delete mode 100644 ruoyi-admin/src/test/java/org/dromara/test/AssertUnitTest.java
delete mode 100644 ruoyi-admin/src/test/java/org/dromara/test/DemoUnitTest.java
delete mode 100644 ruoyi-admin/src/test/java/org/dromara/test/ParamUnitTest.java
delete mode 100644 ruoyi-admin/src/test/java/org/dromara/test/TagUnitTest.java
create mode 100644 ruoyi-common/ruoyi-common-ai/src/test/java/org/dromara/common/ai/SnailAiConfigTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/domain/ResponseModelTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/enums/BusinessStatusEnumTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/factory/YmlPropertySourceFactoryTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreFrameworkUtilityContractTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreInfrastructureContractTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreUtilityBoundaryTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/DateNetDesensitizedUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/SqlUtilTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StreamUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StringUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/TreeBuildUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/validate/enums/EnumPatternValidatorTest.java
create mode 100644 ruoyi-common/ruoyi-common-doc/src/test/java/org/dromara/common/doc/SaTokenSecurityMetadataTest.java
create mode 100644 ruoyi-common/ruoyi-common-elasticsearch/src/test/java/org/dromara/common/elasticsearch/ActuatorEnvironmentPostProcessorTest.java
create mode 100644 ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptInfrastructureTest.java
create mode 100644 ruoyi-common/ruoyi-common-excel/src/test/java/org/dromara/common/excel/ExcelBuilderTest.java
create mode 100644 ruoyi-common/ruoyi-common-excel/src/test/java/org/dromara/common/excel/ExcelConvertTest.java
create mode 100644 ruoyi-common/ruoyi-common-excel/src/test/java/org/dromara/common/excel/ExcelFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-excel/src/test/java/org/dromara/common/excel/ExcelWriterWrapperTest.java
create mode 100644 ruoyi-common/ruoyi-common-job/src/test/java/org/dromara/common/job/SnailJobConfigTest.java
create mode 100644 ruoyi-common/ruoyi-common-json/src/test/java/org/dromara/common/json/JsonTestContext.java
create mode 100644 ruoyi-common/ruoyi-common-json/src/test/java/org/dromara/common/json/config/JacksonConfigTest.java
create mode 100644 ruoyi-common/ruoyi-common-json/src/test/java/org/dromara/common/json/enhance/JsonEnhancementContextTest.java
create mode 100644 ruoyi-common/ruoyi-common-json/src/test/java/org/dromara/common/json/enhance/JsonValueEnhancerTest.java
create mode 100644 ruoyi-common/ruoyi-common-json/src/test/java/org/dromara/common/json/utils/JsonUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-json/src/test/java/org/dromara/common/json/validate/JsonPatternValidatorTest.java
create mode 100644 ruoyi-common/ruoyi-common-liteflow/src/test/java/org/dromara/common/liteflow/LiteFlowComponentTest.java
create mode 100644 ruoyi-common/ruoyi-common-liteflow/src/test/java/org/dromara/common/liteflow/LiteFlowUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-log/src/test/java/org/dromara/common/log/LogFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-mail/src/test/java/org/dromara/common/mail/MailPropertiesTest.java
create mode 100644 ruoyi-common/ruoyi-common-mcp/src/test/java/org/dromara/common/mcp/McpResultTest.java
create mode 100644 ruoyi-common/ruoyi-common-mqtt/src/test/java/org/dromara/common/mqtt/MqttAutoConfigurationTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/mapper/LambdaCrudChainWrapperTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/page/PageQueryTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/query/AggregateSelectUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/query/LambdaJoinQueryBuilderTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/core/query/LambdaQueryBuilderTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/helper/DataPermissionHelperTest.java
create mode 100644 ruoyi-common/ruoyi-common-mybatis/src/test/java/org/dromara/common/mybatis/helper/MybatisUtilityContractTest.java
create mode 100644 ruoyi-common/ruoyi-common-oss/src/test/java/org/dromara/common/oss/OssFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-push/src/test/java/org/dromara/common/push/PushFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-redis/src/test/java/org/dromara/common/redis/RedisAspectContractTest.java
create mode 100644 ruoyi-common/ruoyi-common-redis/src/test/java/org/dromara/common/redis/RedisConfigurationTest.java
create mode 100644 ruoyi-common/ruoyi-common-redis/src/test/java/org/dromara/common/redis/RedisFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-redis/src/test/java/org/dromara/common/redis/manager/CaffeineCacheDecoratorTest.java
create mode 100644 ruoyi-common/ruoyi-common-redis/src/test/java/org/dromara/common/redis/manager/PlusSpringCacheManagerTest.java
create mode 100644 ruoyi-common/ruoyi-common-satoken/src/test/java/org/dromara/common/satoken/SaTokenFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-security/src/test/java/org/dromara/common/security/SecurityConfigTest.java
create mode 100644 ruoyi-common/ruoyi-common-sensitive/src/test/java/org/dromara/common/sensitive/SensitiveStrategyTest.java
create mode 100644 ruoyi-common/ruoyi-common-sms/src/test/java/org/dromara/common/sms/SmsExceptionHandlerTest.java
create mode 100644 ruoyi-common/ruoyi-common-social/src/test/java/org/dromara/common/social/SocialConfigurationTest.java
create mode 100644 ruoyi-common/ruoyi-common-social/src/test/java/org/dromara/common/social/SocialUtilsTest.java
create mode 100644 ruoyi-common/ruoyi-common-translation/src/test/java/org/dromara/common/translation/core/TranslationInterfaceTest.java
create mode 100644 ruoyi-common/ruoyi-common-translation/src/test/java/org/dromara/common/translation/core/handler/TranslationJsonFieldProcessorTest.java
create mode 100644 ruoyi-common/ruoyi-common-translation/src/test/java/org/dromara/common/translation/core/impl/TranslationImplementationsTest.java
create mode 100644 ruoyi-common/ruoyi-common-web/src/test/java/org/dromara/common/web/WebFunctionTest.java
create mode 100644 ruoyi-common/ruoyi-common-web/src/test/java/org/dromara/common/web/WebInfrastructureTest.java
diff --git a/pom.xml b/pom.xml
index d596e7177..6e5f65b3e 100644
--- a/pom.xml
+++ b/pom.xml
@@ -74,7 +74,7 @@
1.7.3
- true
+ false
@@ -113,6 +113,15 @@
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
@@ -508,8 +517,11 @@
${maven-surefire-plugin.version}
-Dfile.encoding=UTF-8
-
- ${profiles.active}
+
exclude
diff --git a/ruoyi-admin/pom.xml b/ruoyi-admin/pom.xml
index 8f2d51cdf..fcaf93b78 100644
--- a/ruoyi-admin/pom.xml
+++ b/ruoyi-admin/pom.xml
@@ -110,13 +110,6 @@
spring-boot-admin-starter-client
-
-
- org.springframework.boot
- spring-boot-starter-test
- test
-
-
diff --git a/ruoyi-admin/src/test/java/org/dromara/test/AssertUnitTest.java b/ruoyi-admin/src/test/java/org/dromara/test/AssertUnitTest.java
deleted file mode 100644
index cfa4b7fed..000000000
--- a/ruoyi-admin/src/test/java/org/dromara/test/AssertUnitTest.java
+++ /dev/null
@@ -1,58 +0,0 @@
-package org.dromara.test;
-
-import org.junit.jupiter.api.Assertions;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.api.Test;
-
-/**
- * 断言单元测试案例
- *
- * @author Lion Li
- */
-@DisplayName("断言单元测试案例")
-public class AssertUnitTest {
-
- /**
- * 验证相等与不相等断言,确保值比较语义清晰。
- */
- @DisplayName("测试 assertEquals 方法")
- @Test
- public void testAssertEquals() {
- Assertions.assertEquals("666", new String("666"));
- Assertions.assertNotEquals("666", "777");
- }
-
- /**
- * 验证同一对象引用与不同对象引用的断言。
- */
- @DisplayName("测试 assertSame 方法")
- @Test
- public void testAssertSame() {
- Object obj = new Object();
- Object obj1 = obj;
- Object obj2 = new Object();
- Assertions.assertSame(obj, obj1);
- Assertions.assertNotSame(obj, obj2);
- }
-
- /**
- * 验证布尔条件断言,覆盖 true 与 false 两类结果。
- */
- @DisplayName("测试 assertTrue 方法")
- @Test
- public void testAssertTrue() {
- Assertions.assertTrue(true);
- Assertions.assertFalse(false);
- }
-
- /**
- * 验证空值与非空值断言,避免空指针场景被误判。
- */
- @DisplayName("测试 assertNull 方法")
- @Test
- public void testAssertNull() {
- Assertions.assertNull(null);
- Assertions.assertNotNull("not null");
- }
-
-}
diff --git a/ruoyi-admin/src/test/java/org/dromara/test/DemoUnitTest.java b/ruoyi-admin/src/test/java/org/dromara/test/DemoUnitTest.java
deleted file mode 100644
index 283c31fab..000000000
--- a/ruoyi-admin/src/test/java/org/dromara/test/DemoUnitTest.java
+++ /dev/null
@@ -1,102 +0,0 @@
-package org.dromara.test;
-
-import org.dromara.common.web.config.properties.CaptchaProperties;
-import org.junit.jupiter.api.*;
-
-import java.util.concurrent.TimeUnit;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * 单元测试基础案例。
- *
- * @author Lion Li
- */
-@DisplayName("单元测试案例")
-public class DemoUnitTest {
-
- /**
- * 所有测试执行前的初始化示例。
- */
- @BeforeAll
- public static void testBeforeAll() {
- System.out.println("@BeforeAll ==================");
- }
-
- /**
- * 所有测试执行后的清理示例。
- */
- @AfterAll
- public static void testAfterAll() {
- System.out.println("@AfterAll ==================");
- }
-
- /**
- * 验证普通 {@link Test} 与 {@link DisplayName} 注解的使用方式。
- */
- @DisplayName("测试 @Test @DisplayName 注解")
- @Test
- public void testTest() {
- CaptchaProperties captchaProperties = new CaptchaProperties();
- captchaProperties.setEnable(Boolean.TRUE);
- captchaProperties.setType("math");
- captchaProperties.setNumberLength(1);
- captchaProperties.setCharLength(4);
-
- assertAll("验证码配置属性",
- () -> assertTrue(captchaProperties.getEnable()),
- () -> assertEquals("math", captchaProperties.getType()),
- () -> assertEquals(1, captchaProperties.getNumberLength()),
- () -> assertEquals(4, captchaProperties.getCharLength())
- );
- }
-
- /**
- * 演示 {@link Disabled} 注解,保留一个不会被执行的测试占位。
- */
- @Disabled
- @DisplayName("测试 @Disabled 注解")
- @Test
- public void testDisabled() {
- fail("禁用测试不应被执行");
- }
-
- /**
- * 验证 {@link Timeout} 注解在指定时间内可以正常通过。
- *
- * @throws InterruptedException 线程等待被中断时抛出
- */
- @Timeout(value = 2L, unit = TimeUnit.SECONDS)
- @DisplayName("测试 @Timeout 注解")
- @Test
- public void testTimeout() throws InterruptedException {
- Thread.sleep(100);
- assertTrue(true);
- }
-
- /**
- * 验证 {@link RepeatedTest} 注解会按指定次数重复执行。
- */
- @DisplayName("测试 @RepeatedTest 注解")
- @RepeatedTest(3)
- public void testRepeatedTest() {
- assertDoesNotThrow(() -> Integer.parseInt("666"));
- }
-
- /**
- * 每个测试执行前的初始化示例。
- */
- @BeforeEach
- public void testBeforeEach() {
- System.out.println("@BeforeEach ==================");
- }
-
- /**
- * 每个测试执行后的清理示例。
- */
- @AfterEach
- public void testAfterEach() {
- System.out.println("@AfterEach ==================");
- }
-
-}
diff --git a/ruoyi-admin/src/test/java/org/dromara/test/ParamUnitTest.java b/ruoyi-admin/src/test/java/org/dromara/test/ParamUnitTest.java
deleted file mode 100644
index 375fac712..000000000
--- a/ruoyi-admin/src/test/java/org/dromara/test/ParamUnitTest.java
+++ /dev/null
@@ -1,106 +0,0 @@
-package org.dromara.test;
-
-import org.dromara.common.core.enums.UserType;
-import org.junit.jupiter.api.AfterEach;
-import org.junit.jupiter.api.BeforeEach;
-import org.junit.jupiter.api.DisplayName;
-import org.junit.jupiter.params.ParameterizedTest;
-import org.junit.jupiter.params.provider.EnumSource;
-import org.junit.jupiter.params.provider.MethodSource;
-import org.junit.jupiter.params.provider.NullSource;
-import org.junit.jupiter.params.provider.ValueSource;
-
-import java.util.List;
-import java.util.stream.Stream;
-
-import static org.junit.jupiter.api.Assertions.*;
-
-/**
- * 带参数单元测试案例
- *
- * @author Lion Li
- */
-@DisplayName("带参数单元测试案例")
-public class ParamUnitTest {
-
- /**
- * 参数化测试共用的字符串样例。
- */
- private static final List TEST_VALUES = List.of("t1", "t2", "t3");
-
- /**
- * 提供 {@link MethodSource} 参数化测试数据。
- *
- * @return 测试参数流
- */
- public static Stream getParam() {
- return TEST_VALUES.stream();
- }
-
- /**
- * 验证 {@link ValueSource} 能按固定字符串集合逐个传参。
- *
- * @param str 当前参数值
- */
- @DisplayName("测试 @ValueSource 注解")
- @ParameterizedTest
- @ValueSource(strings = {"t1", "t2", "t3"})
- public void testValueSource(String str) {
- assertTrue(TEST_VALUES.contains(str));
- }
-
- /**
- * 验证 {@link NullSource} 能传入空值参数。
- *
- * @param str 当前参数值
- */
- @DisplayName("测试 @NullSource 注解")
- @ParameterizedTest
- @NullSource
- public void testNullSource(String str) {
- assertNull(str);
- }
-
- /**
- * 验证 {@link EnumSource} 能遍历用户类型枚举。
- *
- * @param type 当前用户类型
- */
- @DisplayName("测试 @EnumSource 注解")
- @ParameterizedTest
- @EnumSource(UserType.class)
- public void testEnumSource(UserType type) {
- assertNotNull(type);
- assertFalse(type.getUserType().isBlank());
- }
-
- /**
- * 验证 {@link MethodSource} 能读取方法提供的参数流。
- *
- * @param str 当前参数值
- */
- @DisplayName("测试 @MethodSource 注解")
- @ParameterizedTest
- @MethodSource("getParam")
- public void testMethodSource(String str) {
- assertTrue(TEST_VALUES.contains(str));
- }
-
- /**
- * 每个参数化测试执行前的初始化示例。
- */
- @BeforeEach
- public void testBeforeEach() {
- System.out.println("@BeforeEach ==================");
- }
-
- /**
- * 每个参数化测试执行后的清理示例。
- */
- @AfterEach
- public void testAfterEach() {
- System.out.println("@AfterEach ==================");
- }
-
-
-}
diff --git a/ruoyi-admin/src/test/java/org/dromara/test/TagUnitTest.java b/ruoyi-admin/src/test/java/org/dromara/test/TagUnitTest.java
deleted file mode 100644
index ec5670d59..000000000
--- a/ruoyi-admin/src/test/java/org/dromara/test/TagUnitTest.java
+++ /dev/null
@@ -1,72 +0,0 @@
-package org.dromara.test;
-
-import org.junit.jupiter.api.*;
-
-import static org.junit.jupiter.api.Assertions.assertEquals;
-
-/**
- * 标签单元测试案例
- *
- * @author Lion Li
- */
-@DisplayName("标签单元测试案例")
-public class TagUnitTest {
-
- /**
- * 验证 dev 标签测试可以独立筛选执行。
- */
- @Tag("dev")
- @DisplayName("测试 @Tag dev")
- @Test
- public void testTagDev() {
- assertEquals("dev", "dev");
- }
-
- /**
- * 验证 prod 标签测试可以独立筛选执行。
- */
- @Tag("prod")
- @DisplayName("测试 @Tag prod")
- @Test
- public void testTagProd() {
- assertEquals("prod", "prod");
- }
-
- /**
- * 验证 local 标签测试可以独立筛选执行。
- */
- @Tag("local")
- @DisplayName("测试 @Tag local")
- @Test
- public void testTagLocal() {
- assertEquals("local", "local");
- }
-
- /**
- * 验证 exclude 标签测试可以配合构建配置排除。
- */
- @Tag("exclude")
- @DisplayName("测试 @Tag exclude")
- @Test
- public void testTagExclude() {
- assertEquals("exclude", "exclude");
- }
-
- /**
- * 每个标签测试执行前的初始化示例。
- */
- @BeforeEach
- public void testBeforeEach() {
- System.out.println("@BeforeEach ==================");
- }
-
- /**
- * 每个标签测试执行后的清理示例。
- */
- @AfterEach
- public void testAfterEach() {
- System.out.println("@AfterEach ==================");
- }
-
-
-}
diff --git a/ruoyi-common/ruoyi-common-ai/src/test/java/org/dromara/common/ai/SnailAiConfigTest.java b/ruoyi-common/ruoyi-common-ai/src/test/java/org/dromara/common/ai/SnailAiConfigTest.java
new file mode 100644
index 000000000..eefbb8722
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-ai/src/test/java/org/dromara/common/ai/SnailAiConfigTest.java
@@ -0,0 +1,28 @@
+package org.dromara.common.ai;
+
+import org.dromara.common.ai.config.SnailAiConfig;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+
+import static org.junit.jupiter.api.Assertions.assertArrayEquals;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+@DisplayName("common-ai 功能单元测试")
+class SnailAiConfigTest {
+
+ /**
+ * 验证 AI 自动配置只在显式开启 snail-ai.enabled 时生效,避免默认启动外部 AI 客户端。
+ */
+ @Test
+ @DisplayName("声明 Snail AI 启用条件")
+ void shouldDeclareSnailAiEnablementCondition() {
+ ConditionalOnProperty condition = SnailAiConfig.class.getAnnotation(ConditionalOnProperty.class);
+
+ assertNotNull(condition);
+ assertEquals("snail-ai", condition.prefix());
+ assertArrayEquals(new String[]{"enabled"}, condition.name());
+ assertEquals("true", condition.havingValue());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
index 186cb6ee6..0f4f0752b 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/DateUtils.java
@@ -140,7 +140,7 @@ public class DateUtils extends DateUtil {
// 未来时间或非今年
if (date.after(now) || year(date) != year(now)) {
- return formatDateTime(now);
+ return formatDateTime(date);
}
// 今天
diff --git a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
index c0a5a6f79..8b2d0b952 100644
--- a/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
+++ b/ruoyi-common/ruoyi-common-core/src/main/java/org/dromara/common/core/utils/sql/SqlUtil.java
@@ -5,6 +5,8 @@ import lombok.AccessLevel;
import lombok.NoArgsConstructor;
import org.dromara.common.core.utils.StringUtils;
+import java.util.Locale;
+
/**
* sql操作工具类
*
@@ -55,7 +57,7 @@ public class SqlUtil {
}
// ==================== 原有逻辑不变 ====================
- String normalizedValue = value.replaceAll("\\p{Z}|\\s", "");
+ String normalizedValue = value.replaceAll("[\\p{Z}\\s]+", " ").toLowerCase(Locale.ROOT);
String[] sqlKeywords = StringUtils.split(SQL_REGEX, "\\|");
for (String sqlKeyword : sqlKeywords) {
if (StringUtils.indexOf(normalizedValue, sqlKeyword) > -1) {
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/domain/ResponseModelTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/domain/ResponseModelTest.java
new file mode 100644
index 000000000..c37f2f242
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/domain/ResponseModelTest.java
@@ -0,0 +1,72 @@
+package org.dromara.common.core.domain;
+
+import org.dromara.common.core.constant.HttpStatus;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("公共响应模型单元测试")
+class ResponseModelTest {
+
+ /**
+ * 验证响应工厂方法设置正确的状态码、消息和业务数据。
+ */
+ @Test
+ @DisplayName("构建成功、失败和警告响应")
+ void responseFactoriesShouldSetExpectedFields() {
+ R success = R.ok("done", "payload");
+ R failure = R.fail("failed", "payload");
+ R warning = R.warn("warning", "payload");
+
+ assertAll(
+ () -> assertEquals(HttpStatus.SUCCESS, success.getCode()),
+ () -> assertEquals("done", success.getMsg()),
+ () -> assertEquals("payload", success.getData()),
+ () -> assertEquals(HttpStatus.ERROR, failure.getCode()),
+ () -> assertEquals(HttpStatus.WARN, warning.getCode())
+ );
+ }
+
+ /**
+ * 验证成功状态判断能够处理失败响应和空响应。
+ */
+ @Test
+ @DisplayName("正确识别响应成功状态")
+ void responseStatusChecksShouldHandleNullAndError() {
+ assertTrue(R.isSuccess(R.ok()));
+ assertFalse(R.isSuccess(R.fail()));
+ assertFalse(R.isSuccess(null));
+ assertTrue(R.isError(null));
+ }
+
+ /**
+ * 验证仅传集合时分页总数使用集合实际大小。
+ */
+ @Test
+ @DisplayName("分页结果按集合大小计算总数")
+ void pageResultShouldUseCollectionSizeAsTotal() {
+ PageResult result = PageResult.build(List.of("a", "b"));
+
+ assertEquals(2L, result.getTotal());
+ assertEquals(List.of("a", "b"), result.getRows());
+ }
+
+ /**
+ * 验证空行集合会被标准化为空列表,避免调用方判空。
+ */
+ @Test
+ @DisplayName("分页结果将空集合参数转换为空列表")
+ void pageResultShouldNormalizeNullRows() {
+ PageResult result = PageResult.build(null, 10L);
+ PageResult constructed = new PageResult<>(null, 5L);
+
+ assertNotNull(result.getRows());
+ assertTrue(result.getRows().isEmpty());
+ assertTrue(constructed.getRows().isEmpty());
+ assertEquals(10L, result.getTotal());
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/enums/BusinessStatusEnumTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/enums/BusinessStatusEnumTest.java
new file mode 100644
index 000000000..b1b3ea8c7
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/enums/BusinessStatusEnumTest.java
@@ -0,0 +1,77 @@
+package org.dromara.common.core.enums;
+
+import org.dromara.common.core.exception.ServiceException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("BusinessStatusEnum 单元测试")
+class BusinessStatusEnumTest {
+
+ /**
+ * 验证状态码查询、描述查询以及运行中和已结束状态集合。
+ */
+ @Test
+ @DisplayName("查询业务状态")
+ void shouldResolveBusinessStatuses() {
+ assertEquals(BusinessStatusEnum.DRAFT, BusinessStatusEnum.getByStatus("draft"));
+ assertNull(BusinessStatusEnum.getByStatus("unknown"));
+ assertEquals("已完成", BusinessStatusEnum.findByStatus("finish"));
+ assertEquals("", BusinessStatusEnum.findByStatus(" "));
+ assertEquals(List.of("draft", "waiting", "back", "cancel"), BusinessStatusEnum.runningStatus());
+ assertEquals(List.of("finish", "invalid", "termination"), BusinessStatusEnum.finishStatus());
+ }
+
+ /**
+ * 验证流程状态分类能够正确识别可重新发起状态和终止类状态。
+ */
+ @Test
+ @DisplayName("分类业务状态")
+ void shouldClassifyBusinessStatuses() {
+ assertTrue(BusinessStatusEnum.isDraftOrCancelOrBack("draft"));
+ assertTrue(BusinessStatusEnum.isDraftOrCancelOrBack("cancel"));
+ assertTrue(BusinessStatusEnum.isDraftOrCancelOrBack("back"));
+ assertFalse(BusinessStatusEnum.isDraftOrCancelOrBack("waiting"));
+ assertTrue(BusinessStatusEnum.initialState("invalid"));
+ assertTrue(BusinessStatusEnum.initialState("termination"));
+ assertFalse(BusinessStatusEnum.initialState("waiting"));
+ }
+
+ /**
+ * 验证启动流程仅允许草稿、撤销和退回等可发起状态,并为禁止状态返回业务异常。
+ */
+ @Test
+ @DisplayName("校验流程启动状态")
+ void shouldValidateStartStatus() {
+ assertDoesNotThrow(() -> BusinessStatusEnum.checkStartStatus("draft"));
+ assertEquals("该单据已提交过申请,正在审批中!",
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkStartStatus("waiting")).getMessage());
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkStartStatus("finish"));
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkStartStatus("invalid"));
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkStartStatus("termination"));
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkStartStatus(" "));
+ }
+
+ /**
+ * 验证撤销、退回及作废校验分别拒绝所有已结束或重复操作状态。
+ */
+ @Test
+ @DisplayName("校验流程变更状态")
+ void shouldValidateCancelBackAndInvalidStatuses() {
+ assertDoesNotThrow(() -> BusinessStatusEnum.checkCancelStatus("waiting"));
+ for (String status : List.of("cancel", "finish", "invalid", "termination", "back", " ")) {
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkCancelStatus(status));
+ }
+ assertDoesNotThrow(() -> BusinessStatusEnum.checkBackStatus("waiting"));
+ for (String status : List.of("back", "finish", "invalid", "termination", "cancel", " ")) {
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkBackStatus(status));
+ }
+ assertDoesNotThrow(() -> BusinessStatusEnum.checkInvalidStatus("waiting"));
+ for (String status : List.of("finish", "invalid", "termination", " ")) {
+ assertThrows(ServiceException.class, () -> BusinessStatusEnum.checkInvalidStatus(status));
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/factory/YmlPropertySourceFactoryTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/factory/YmlPropertySourceFactoryTest.java
new file mode 100644
index 000000000..924fbb805
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/factory/YmlPropertySourceFactoryTest.java
@@ -0,0 +1,67 @@
+package org.dromara.common.core.factory;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.env.PropertySource;
+import org.springframework.core.io.ByteArrayResource;
+import org.springframework.core.io.support.EncodedResource;
+
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisplayName("YmlPropertySourceFactory 单元测试")
+class YmlPropertySourceFactoryTest {
+
+ /**
+ * 验证公共配置源工厂可以将 YAML 层级结构展开为 Spring 属性键。
+ *
+ * @throws IOException 配置资源读取失败
+ */
+ @Test
+ @DisplayName("解析 YAML 配置资源")
+ void shouldLoadYamlAsProperties() throws IOException {
+ ByteArrayResource resource = namedResource("common-test.yml", "feature:\n enabled: true\n timeout: 30\n");
+
+ PropertySource> source = new YmlPropertySourceFactory()
+ .createPropertySource(null, new EncodedResource(resource, StandardCharsets.UTF_8));
+
+ assertEquals("common-test.yml", source.getName());
+ assertEquals(true, source.getProperty("feature.enabled"));
+ assertEquals(30, source.getProperty("feature.timeout"));
+ }
+
+ /**
+ * 验证非 YAML 资源仍委托 Spring 默认逻辑解析,避免公共工厂破坏 properties 配置。
+ *
+ * @throws IOException 配置资源读取失败
+ */
+ @Test
+ @DisplayName("回退解析 properties 配置资源")
+ void shouldDelegatePropertiesResourcesToSpring() throws IOException {
+ ByteArrayResource resource = namedResource("common-test.properties", "feature.mode=strict\n");
+
+ PropertySource> source = new YmlPropertySourceFactory()
+ .createPropertySource("fallback", new EncodedResource(resource, StandardCharsets.UTF_8));
+
+ assertEquals("fallback", source.getName());
+ assertEquals("strict", source.getProperty("feature.mode"));
+ }
+
+ /**
+ * 创建具有稳定文件名的内存资源,以触发配置源工厂对应的扩展名分支。
+ *
+ * @param filename 资源文件名
+ * @param content 资源内容
+ * @return 命名内存资源
+ */
+ private static ByteArrayResource namedResource(String filename, String content) {
+ return new ByteArrayResource(content.getBytes(StandardCharsets.UTF_8)) {
+ @Override
+ public String getFilename() {
+ return filename;
+ }
+ };
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreFrameworkUtilityContractTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreFrameworkUtilityContractTest.java
new file mode 100644
index 000000000..b64e7d5ab
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreFrameworkUtilityContractTest.java
@@ -0,0 +1,251 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.lang.Dict;
+import cn.hutool.extra.spring.SpringUtil;
+import io.github.linpeilie.Converter;
+import jakarta.validation.ConstraintViolation;
+import jakarta.validation.ConstraintViolationException;
+import jakarta.validation.Validator;
+import org.dromara.common.core.utils.reflect.AnnotationUtils;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.context.i18n.LocaleContextHolder;
+import org.springframework.context.support.StaticApplicationContext;
+import org.springframework.context.support.StaticMessageSource;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+@DisplayName("common-core 框架工具契约单元测试")
+class CoreFrameworkUtilityContractTest {
+
+ private static Converter converter;
+ private static Validator validator;
+ private static StaticMessageSource messageSource;
+
+ /**
+ * 初始化框架工具静态依赖的最小 Spring 容器,确保测试不需要启动完整应用。
+ */
+ @BeforeAll
+ static void initializeFrameworkUtilities() {
+ converter = mock(Converter.class);
+ validator = mock(Validator.class);
+ StaticApplicationContext context = new StaticApplicationContext();
+ context.getBeanFactory().registerSingleton("converter", converter);
+ context.getBeanFactory().registerSingleton("validator", validator);
+ messageSource = context.getStaticMessageSource();
+ messageSource.addMessage("welcome", Locale.SIMPLIFIED_CHINESE, "欢迎 {0}");
+ context.refresh();
+ new SpringUtil().setApplicationContext(context);
+ }
+
+ /**
+ * 清理线程语言环境和 mock 调用记录,避免测试之间共享状态。
+ */
+ @AfterEach
+ void resetFrameworkState() {
+ LocaleContextHolder.resetLocaleContext();
+ reset(converter, validator);
+ }
+
+ /**
+ * 验证 Mapstruct Plus 的对象转换和目标对象填充均委托给框架 Converter。
+ */
+ @Test
+ @DisplayName("委托对象转换和目标填充")
+ void shouldDelegateObjectConversions() {
+ Source source = new Source("alice");
+ Destination converted = new Destination("converted");
+ Destination target = new Destination("existing");
+ when(converter.convert(source, Destination.class)).thenReturn(converted);
+ when(converter.convert(source, target)).thenReturn(target);
+
+ assertSame(converted, MapstructUtils.convert(source, Destination.class));
+ assertSame(target, MapstructUtils.convert(source, target));
+ verify(converter).convert(source, Destination.class);
+ verify(converter).convert(source, target);
+ }
+
+ /**
+ * 验证对象转换在来源或目标为空时直接返回空值,不误调用底层 Converter。
+ */
+ @Test
+ @DisplayName("短路空对象转换")
+ void shouldShortCircuitNullObjectConversions() {
+ Source source = new Source("alice");
+
+ assertNull(MapstructUtils.convert((Source) null, Destination.class));
+ assertNull(MapstructUtils.convert(source, (Class) null));
+ assertNull(MapstructUtils.convert(source, (Destination) null));
+ assertNull(MapstructUtils.convert((Source) null, new Destination("existing")));
+ verifyNoInteractions(converter);
+ }
+
+ /**
+ * 验证列表转换保留 null、空列表和普通列表各自约定的返回语义。
+ */
+ @Test
+ @DisplayName("转换列表并处理空输入")
+ void shouldConvertListsAndHandleEmptyInputs() {
+ List sources = List.of(new Source("alice"));
+ List targets = List.of(new Destination("converted"));
+ when(converter.convert(sources, Destination.class)).thenReturn(targets);
+
+ assertNull(MapstructUtils.convert((List) null, Destination.class));
+ assertEquals(List.of(), MapstructUtils.convert(List.of(), Destination.class));
+ assertSame(targets, MapstructUtils.convert(sources, Destination.class));
+ verify(converter).convert(sources, Destination.class);
+ }
+
+ /**
+ * 验证 Map 转 Bean 仅在数据和目标类型有效时调用 Mapstruct Plus。
+ */
+ @Test
+ @DisplayName("转换 Map 并处理无效输入")
+ void shouldConvertMapsAndHandleInvalidInputs() {
+ Map source = Map.of("name", "alice");
+ Destination target = new Destination("converted");
+ when(converter.convert(source, Destination.class)).thenReturn(target);
+
+ assertNull(MapstructUtils.convert((Map) null, Destination.class));
+ assertNull(MapstructUtils.convert(Map.of(), Destination.class));
+ assertNull(MapstructUtils.convert(source, null));
+ assertSame(target, MapstructUtils.convert(source, Destination.class));
+ verify(converter).convert(source, Destination.class);
+ }
+
+ /**
+ * 验证安全 Getter 只在对象和函数均有效时求值,并在无法求值时返回约定默认值。
+ */
+ @Test
+ @DisplayName("安全读取对象属性")
+ void shouldReadObjectPropertiesSafely() {
+ Source source = new Source("alice");
+
+ assertEquals("alice", ObjectUtils.notNullGetter(source, Source::name));
+ assertNull(ObjectUtils.notNullGetter(null, Source::name));
+ assertNull(ObjectUtils.notNullGetter(source, null));
+ assertEquals("fallback", ObjectUtils.notNullGetter(null, Source::name, "fallback"));
+ assertEquals("fallback", ObjectUtils.notNullGetter(source, null, "fallback"));
+ assertEquals("alice", ObjectUtils.notNullGetter(source, Source::name, "fallback"));
+ assertEquals("alice", ObjectUtils.notNull("alice", "fallback"));
+ assertEquals("fallback", ObjectUtils.notNull(null, "fallback"));
+ }
+
+ /**
+ * 验证 Bean Validation 在无约束违规时正常返回并传递指定校验组。
+ */
+ @Test
+ @DisplayName("通过 Bean Validation 校验有效对象")
+ void shouldValidateObjectWithRequestedGroups() {
+ Source source = new Source("alice");
+ when(validator.validate(source, ValidationGroup.class)).thenReturn(Set.of());
+
+ assertDoesNotThrow(() -> ValidatorUtils.validate(source, ValidationGroup.class));
+ verify(validator).validate(source, ValidationGroup.class);
+ }
+
+ /**
+ * 验证 Bean Validation 对空对象和约束违规分别抛出框架约定的异常。
+ */
+ @Test
+ @DisplayName("报告空对象和约束违规")
+ void shouldReportNullObjectAndConstraintViolations() {
+ RuntimeException nullException = assertThrows(RuntimeException.class,
+ () -> ValidatorUtils.validate(null));
+ @SuppressWarnings("unchecked")
+ ConstraintViolation violation = mock(ConstraintViolation.class);
+ Source source = new Source("");
+ when(validator.validate(source)).thenReturn(Set.of(violation));
+
+ ConstraintViolationException validationException = assertThrows(ConstraintViolationException.class,
+ () -> ValidatorUtils.validate(source));
+
+ assertEquals("请求参数不能为空", nullException.getMessage());
+ assertEquals("参数校验异常", validationException.getMessage());
+ assertEquals(Set.of(violation), validationException.getConstraintViolations());
+ }
+
+ /**
+ * 验证国际化工具使用线程当前语言环境和消息参数查询 MessageSource。
+ */
+ @Test
+ @DisplayName("按当前语言环境解析国际化消息")
+ void shouldResolveMessageUsingCurrentLocale() {
+ Locale locale = Locale.SIMPLIFIED_CHINESE;
+ LocaleContextHolder.setLocale(locale);
+ assertEquals("欢迎 alice", MessageUtils.message("welcome", "alice"));
+ }
+
+ /**
+ * 验证缺少国际化资源时返回消息键,保持调用方可读的降级结果。
+ */
+ @Test
+ @DisplayName("缺少国际化资源时返回消息键")
+ void shouldFallbackToCodeWhenMessageIsMissing() {
+ assertEquals("missing.code", MessageUtils.message("missing.code"));
+ }
+
+ /**
+ * 验证按注解全类名读取注解实例及属性字典,防止 Hutool 升级改变反射结果。
+ */
+ @Test
+ @DisplayName("按全类名读取注解和属性")
+ void shouldReadAnnotationAndValuesByClassName() {
+ String annotationName = ContractMarker.class.getName();
+
+ ContractMarker annotation = assertInstanceOf(ContractMarker.class,
+ AnnotationUtils.getAnnotation(AnnotatedType.class, annotationName));
+ Dict values = AnnotationUtils.getAnnotationValueMap(AnnotatedType.class, annotationName);
+
+ assertEquals("core", annotation.value());
+ assertEquals(3, annotation.level());
+ assertNotNull(values);
+ assertEquals("core", values.getStr("value"));
+ assertEquals(3, values.getInt("level"));
+ }
+
+ /**
+ * 验证注解类不存在或目标元素未标注时返回空值,不把反射细节泄漏给调用方。
+ */
+ @Test
+ @DisplayName("处理缺失注解类型和未标注元素")
+ void shouldHandleMissingAnnotationTypesAndValues() {
+ assertNull(AnnotationUtils.getAnnotation(String.class, ContractMarker.class.getName()));
+ assertNull(AnnotationUtils.getAnnotation(AnnotatedType.class, "missing.Annotation"));
+ assertNull(AnnotationUtils.getAnnotationValueMap(AnnotatedType.class, "missing.Annotation"));
+ }
+
+ private interface ValidationGroup {
+ }
+
+ private record Source(String name) {
+ }
+
+ private record Destination(String name) {
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Target(ElementType.TYPE)
+ private @interface ContractMarker {
+
+ String value();
+
+ int level();
+ }
+
+ @ContractMarker(value = "core", level = 3)
+ private static final class AnnotatedType {
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreInfrastructureContractTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreInfrastructureContractTest.java
new file mode 100644
index 000000000..61fcc5c54
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreInfrastructureContractTest.java
@@ -0,0 +1,133 @@
+package org.dromara.common.core.utils;
+
+import jakarta.validation.ConstraintValidatorContext;
+import cn.hutool.extra.spring.SpringUtil;
+import org.dromara.common.core.service.DictService;
+import org.dromara.common.core.validate.dicts.DictPattern;
+import org.dromara.common.core.validate.dicts.DictPatternValidator;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.MockedStatic;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+import org.springframework.web.context.request.RequestContextHolder;
+import org.springframework.web.context.request.ServletRequestAttributes;
+
+import java.util.List;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.mockStatic;
+import static org.mockito.Mockito.when;
+
+@DisplayName("common-core 基础设施契约单元测试")
+class CoreInfrastructureContractTest {
+
+ /**
+ * 清理线程绑定的请求上下文,避免测试之间共享 Servlet 状态。
+ */
+ @AfterEach
+ void resetRequestContext() {
+ RequestContextHolder.resetRequestAttributes();
+ }
+
+ /**
+ * 验证 Servlet 工具从 Spring 请求上下文读取参数、响应和会话,并保持类型转换约定。
+ */
+ @Test
+ @DisplayName("读取线程绑定的 Servlet 上下文")
+ void shouldReadRequestContextAndConvertParameters() {
+ MockHttpServletRequest request = new MockHttpServletRequest();
+ MockHttpServletResponse response = new MockHttpServletResponse();
+ request.setParameter("name", "alice");
+ request.setParameter("age", "18");
+ request.setParameter("enabled", "true");
+ request.setParameter("roles", "admin", "user");
+ RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(request, response));
+
+ assertSame(request, ServletUtils.getRequest());
+ assertSame(response, ServletUtils.getResponse());
+ assertEquals("alice", ServletUtils.getParameter("name"));
+ assertEquals("fallback", ServletUtils.getParameter("missing", "fallback"));
+ assertEquals(18, ServletUtils.getParameterToInt("age"));
+ assertTrue(ServletUtils.getParameterToBool("enabled"));
+ assertEquals("admin,user", ServletUtils.getParamMap(request).get("roles"));
+ assertSame(request.getSession(), ServletUtils.getSession());
+ }
+
+ /**
+ * 验证请求头解码、Ajax 识别、代理 IP 解析和 JSON 响应渲染保持稳定。
+ */
+ @Test
+ @DisplayName("处理常用 HTTP 协议细节")
+ void shouldHandleHeadersAjaxClientIpAndJsonRendering() throws Exception {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/report.json");
+ request.addHeader("X-Name", ServletUtils.urlEncode("中文 value"));
+ request.addHeader("X-Forwarded-For", "[2001:db8::1]");
+ MockHttpServletResponse response = new MockHttpServletResponse();
+
+ assertEquals("中文 value", ServletUtils.getHeader(request, "X-Name"));
+ assertEquals("", ServletUtils.getHeader(request, "missing"));
+ assertEquals(ServletUtils.getHeaders(request).get("x-name"), request.getHeader("X-Name"));
+ assertTrue(ServletUtils.isAjaxRequest(request));
+ assertEquals("2001:db8::1", ServletUtils.getClientIP(request));
+
+ ServletUtils.renderString(response, "{\"ok\":true}");
+
+ assertEquals(200, response.getStatus());
+ assertTrue(response.getContentType().startsWith("application/json"));
+ assertEquals("{\"ok\":true}", response.getContentAsString());
+ }
+
+ /**
+ * 验证虚拟线程批量执行保持提交顺序,并把任务异常的真实原因传递给调用方。
+ */
+ @Test
+ @DisplayName("批量执行虚拟线程任务")
+ void shouldPreserveVirtualTaskOrderAndFailureCause() {
+ List results = ThreadUtils.virtualSubmitAll(
+ () -> 1,
+ () -> 2,
+ () -> 3);
+
+ RuntimeException exception = assertThrows(RuntimeException.class,
+ () -> ThreadUtils.virtualInvokeAll(() -> {
+ throw new IllegalStateException("task-failed");
+ }));
+
+ assertEquals(List.of(1, 2, 3), results);
+ assertInstanceOf(IllegalStateException.class, exception.getCause());
+ assertTrue(exception.getMessage().contains("task-failed"));
+ }
+
+ /**
+ * 验证字典校验器按注解分隔符调用字典服务,并正确处理空值、缺失类型和未知字典值。
+ */
+ @Test
+ @DisplayName("通过字典服务校验字段值")
+ void shouldValidateDictionaryValuesThroughConfiguredService() {
+ DictPattern annotation = mock(DictPattern.class);
+ when(annotation.dictType()).thenReturn("sys_status");
+ when(annotation.separator()).thenReturn("|");
+ DictService dictService = mock(DictService.class);
+ when(dictService.getDictLabel("sys_status", "0|1", "|")).thenReturn("正常|停用");
+ when(dictService.getDictLabel("sys_status", "9", "|")).thenReturn("");
+ DictPatternValidator validator = new DictPatternValidator();
+ validator.initialize(annotation);
+
+ try (MockedStatic spring = mockStatic(SpringUtil.class)) {
+ spring.when(() -> SpringUtil.getBean(DictService.class)).thenReturn(dictService);
+
+ assertTrue(validator.isValid(null, mock(ConstraintValidatorContext.class)));
+ assertTrue(validator.isValid("0|1", null));
+ assertFalse(validator.isValid("9", null));
+ }
+
+ DictPattern invalidAnnotation = mock(DictPattern.class);
+ when(invalidAnnotation.dictType()).thenReturn(" ");
+ DictPatternValidator invalidValidator = new DictPatternValidator();
+ invalidValidator.initialize(invalidAnnotation);
+ assertFalse(invalidValidator.isValid("0", null));
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreUtilityBoundaryTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreUtilityBoundaryTest.java
new file mode 100644
index 000000000..b3260d2ea
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/CoreUtilityBoundaryTest.java
@@ -0,0 +1,156 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.exceptions.ValidateException;
+import jakarta.servlet.http.HttpServletResponse;
+import org.dromara.common.core.utils.file.FileUtils;
+import org.dromara.common.core.utils.file.MimeTypeUtils;
+import org.dromara.common.core.utils.reflect.ReflectUtils;
+import org.dromara.common.core.utils.regex.RegexUtils;
+import org.dromara.common.core.utils.regex.RegexValidator;
+import org.dromara.common.core.xss.XssValidator;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+
+@DisplayName("common-core 公共工具边界单元测试")
+class CoreUtilityBoundaryTest {
+
+ /**
+ * 验证下载文件名对中文、空格和加号执行 URL 编码,并同步写入浏览器可读取的响应头。
+ */
+ @Test
+ @DisplayName("编码并设置下载文件名响应头")
+ void shouldEncodeAndExposeAttachmentFileName() {
+ HttpServletResponse response = mock(HttpServletResponse.class);
+ String encoded = "%E6%B5%8B%E8%AF%95%20report%2B1.xlsx";
+
+ assertEquals(encoded, FileUtils.percentEncode("测试 report+1.xlsx"));
+
+ FileUtils.setAttachmentResponseHeader(response, "测试 report+1.xlsx");
+
+ verify(response).addHeader("Access-Control-Expose-Headers", "Content-Disposition,download-filename");
+ verify(response).setHeader("Content-disposition",
+ "attachment; filename=" + encoded + ";filename*=utf-8''" + encoded);
+ verify(response).setHeader("download-filename", encoded);
+ }
+
+ /**
+ * 验证上传文件类型判断忽略扩展名大小写,同时拒绝未列入白名单的可执行文件。
+ */
+ @Test
+ @DisplayName("识别允许的文件扩展名")
+ void shouldRecognizeAllowedFileExtensionsCaseInsensitively() {
+ assertTrue(MimeTypeUtils.isImage("JpEg"));
+ assertTrue(MimeTypeUtils.isVideo("MP4"));
+ assertTrue(MimeTypeUtils.isMedia("Mp3"));
+ assertTrue(MimeTypeUtils.isDefaultAllowed("PDF"));
+ assertFalse(MimeTypeUtils.isDefaultAllowed("exe"));
+ assertFalse(MimeTypeUtils.isImage(null));
+ }
+
+ /**
+ * 验证正则提取失败时稳定回退默认值,并校验账号与状态的有效边界。
+ */
+ @Test
+ @DisplayName("处理正则提取与业务格式边界")
+ void shouldHandleRegexExtractionAndValidationBoundaries() {
+ assertEquals("42", RegexUtils.extractFromString("order-42", "order-(\\d+)", "none"));
+ assertEquals("none", RegexUtils.extractFromString("missing", "order-(\\d+)", "none"));
+ assertEquals("none", RegexUtils.extractFromString("order-42", "([", "none"));
+
+ assertTrue(RegexValidator.isAccount("user_1"));
+ assertFalse(RegexValidator.isAccount("1user"));
+ assertFalse(RegexValidator.isAccount("user"));
+ assertTrue(RegexValidator.isStatus("0"));
+ assertTrue(RegexValidator.isStatus("1"));
+ assertFalse(RegexValidator.isStatus("2"));
+ ValidateException exception = assertThrows(ValidateException.class,
+ () -> RegexValidator.validateAccount("bad", "账号格式错误"));
+ assertEquals("账号格式错误", exception.getMessage());
+ }
+
+ /**
+ * 验证反射工具可以沿 JavaBean 属性路径读取和修改嵌套对象,保障 Excel 等调用方的字段访问。
+ */
+ @Test
+ @DisplayName("读写嵌套 JavaBean 属性")
+ void shouldReadAndWriteNestedBeanProperties() {
+ TestRoot root = new TestRoot(new TestChild("before"));
+
+ assertEquals("before", ReflectUtils.invokeGetter(root, "child.name"));
+
+ ReflectUtils.invokeSetter(root, "child.name", "after");
+
+ assertEquals("after", root.getChild().getName());
+ }
+
+ /**
+ * 验证 XSS 校验允许空值和普通文本,并拒绝任意 HTML 标签输入。
+ */
+ @Test
+ @DisplayName("拒绝包含 HTML 标签的文本")
+ void shouldRejectHtmlMarkup() {
+ XssValidator validator = new XssValidator();
+
+ assertTrue(validator.isValid(null, null));
+ assertTrue(validator.isValid("plain text", null));
+ assertFalse(validator.isValid("", null));
+ assertFalse(validator.isValid("hello world", null));
+ }
+
+ private static class TestRoot {
+ private final TestChild child;
+
+ /**
+ * 创建带子对象的测试根对象。
+ *
+ * @param child 子对象
+ */
+ private TestRoot(TestChild child) {
+ this.child = child;
+ }
+
+ /**
+ * 返回子对象,供嵌套反射路径访问。
+ *
+ * @return 子对象
+ */
+ public TestChild getChild() {
+ return child;
+ }
+ }
+
+ private static class TestChild {
+ private String name;
+
+ /**
+ * 创建具有初始名称的测试子对象。
+ *
+ * @param name 初始名称
+ */
+ private TestChild(String name) {
+ this.name = name;
+ }
+
+ /**
+ * 返回名称,供嵌套反射路径读取。
+ *
+ * @return 名称
+ */
+ public String getName() {
+ return name;
+ }
+
+ /**
+ * 修改名称,供嵌套反射路径写入。
+ *
+ * @param name 新名称
+ */
+ public void setName(String name) {
+ this.name = name;
+ }
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/DateNetDesensitizedUtilsTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/DateNetDesensitizedUtilsTest.java
new file mode 100644
index 000000000..ada464539
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/DateNetDesensitizedUtilsTest.java
@@ -0,0 +1,114 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.date.DateUtil;
+import org.dromara.common.core.exception.ServiceException;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Date;
+import java.util.concurrent.TimeUnit;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("日期、网络与脱敏工具单元测试")
+class DateNetDesensitizedUtilsTest {
+
+ /**
+ * 验证时间差可以忽略参数顺序并按秒精度格式化。
+ */
+ @Test
+ @DisplayName("格式化时间差")
+ void shouldFormatTimeDifferenceBySecond() {
+ Date start = new Date(0);
+ Date end = new Date(3_661_000);
+
+ assertEquals("1小时1分1秒", DateUtils.formatBetweenBySecond(start, end));
+ assertEquals("1小时1分1秒", DateUtils.formatBetweenBySecond(end, start));
+ }
+
+ /**
+ * 验证日期范围接受边界值,并拒绝倒序、超限和不支持的单位。
+ */
+ @Test
+ @DisplayName("校验日期范围")
+ void shouldValidateDateRangeAndUnits() {
+ Date start = DateUtil.parse("2026-01-01 00:00:00");
+ Date end = DateUtil.parse("2026-01-03 00:00:00");
+
+ assertDoesNotThrow(() -> DateUtils.validateDateRange(start, end, 2, TimeUnit.DAYS));
+ assertThrows(ServiceException.class, () -> DateUtils.validateDateRange(end, start, 2, TimeUnit.DAYS));
+ assertThrows(ServiceException.class, () -> DateUtils.validateDateRange(start, end, 1, TimeUnit.DAYS));
+ assertThrows(IllegalArgumentException.class,
+ () -> DateUtils.validateDateRange(start, end, 1, TimeUnit.SECONDS));
+ }
+
+ /**
+ * 验证一天内各小时会映射到凌晨、上午、中午、下午和晚上。
+ */
+ @Test
+ @DisplayName("识别当天时间段")
+ void shouldResolveTodayPeriod() {
+ assertEquals("凌晨", DateUtils.getTodayHour(DateUtil.parse("2026-01-01 06:00:00")));
+ assertEquals("上午", DateUtils.getTodayHour(DateUtil.parse("2026-01-01 09:00:00")));
+ assertEquals("中午", DateUtils.getTodayHour(DateUtil.parse("2026-01-01 12:00:00")));
+ assertEquals("下午", DateUtils.getTodayHour(DateUtil.parse("2026-01-01 15:00:00")));
+ assertEquals("晚上", DateUtils.getTodayHour(DateUtil.parse("2026-01-01 20:00:00")));
+ }
+
+ /**
+ * 验证友好时间处理空值、刚刚、分钟前以及未来时间,并确保未来时间使用目标日期。
+ */
+ @Test
+ @DisplayName("格式化友好时间")
+ void shouldFormatFriendlyTimeUsingTargetDate() {
+ Date now = new Date();
+ Date future = DateUtil.offsetDay(now, 2);
+
+ assertEquals("", DateUtils.formatFriendlyTime(null));
+ assertEquals("刚刚", DateUtils.formatFriendlyTime(DateUtil.offsetSecond(now, -10)));
+ assertTrue(DateUtils.formatFriendlyTime(DateUtil.offsetMinute(now, -5)).endsWith("分钟前"));
+ assertEquals(DateUtils.formatDateTime(future), DateUtils.formatFriendlyTime(future));
+ }
+
+ /**
+ * 验证 IPv4、IPv6、精确地址、通配符和 CIDR 规则的匹配结果。
+ */
+ @Test
+ @DisplayName("匹配 IP 地址规则")
+ void shouldMatchIpAddressRules() {
+ assertTrue(NetUtils.isIPv4("192.168.1.1"));
+ assertFalse(NetUtils.isIPv4("999.1.1.1"));
+ assertTrue(NetUtils.isIPv6("::1"));
+ assertTrue(NetUtils.isInnerIPv6("::1"));
+ assertTrue(NetUtils.isMatchIpRule("192.168.1.10", "192.168.1.10"));
+ assertTrue(NetUtils.isMatchIpRule("192.168.*.?", "192.168.1.8"));
+ assertTrue(NetUtils.isMatchIpRule("10.0.0.0/8", "10.20.30.40"));
+ assertFalse(NetUtils.isMatchIpRule("10.0.0.0/8", "11.20.30.40"));
+ assertFalse(NetUtils.isMatchCidr("10.0.0.0/99", "10.0.0.1"));
+ assertFalse(NetUtils.isMatchIpRule(" ", "10.0.0.1"));
+ }
+
+ /**
+ * 验证普通脱敏在短值、临界值和标准长度下应用固定掩码规则。
+ */
+ @Test
+ @DisplayName("应用固定长度脱敏")
+ void shouldMaskValuesWithFixedLength() {
+ assertNull(DesensitizedUtils.mask(null, 2, 2, 4));
+ assertEquals("***", DesensitizedUtils.mask("abc", 2, 2, 4));
+ assertEquals("ab****", DesensitizedUtils.mask("abcdef", 2, 2, 4));
+ assertEquals("ab****g", DesensitizedUtils.mask("abcdefg", 2, 2, 4));
+ assertEquals("ab****ij", DesensitizedUtils.mask("abcdefghij", 2, 2, 4));
+ }
+
+ /**
+ * 验证高安全脱敏对短 Token 全掩码,并在长 Token 中仅保留指定首尾字符。
+ */
+ @Test
+ @DisplayName("应用高安全脱敏")
+ void shouldMaskHighSecurityValues() {
+ assertEquals("***", DesensitizedUtils.maskHighSecurity("abc", 3, 2));
+ assertEquals("ab***", DesensitizedUtils.maskHighSecurity("abcde", 3, 2));
+ assertEquals("ab******ij", DesensitizedUtils.maskHighSecurity("abcdefghij", 2, 2));
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/SqlUtilTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/SqlUtilTest.java
new file mode 100644
index 000000000..714cfe244
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/SqlUtilTest.java
@@ -0,0 +1,54 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.exceptions.UtilException;
+import org.dromara.common.core.utils.sql.SqlUtil;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("SqlUtil 单元测试")
+class SqlUtilTest {
+
+ /**
+ * 验证合法字段、逗号和表字段表达式可以通过排序校验。
+ */
+ @Test
+ @DisplayName("接受合法的多字段排序表达式")
+ void escapeOrderBySqlShouldAcceptSafeColumns() {
+ assertEquals("user_name,create_time", SqlUtil.escapeOrderBySql("user_name,create_time"));
+ assertTrue(SqlUtil.isValidOrderBySql("table_name.column_name"));
+ }
+
+ /**
+ * 验证包含 SQL 分隔符等非法字符的排序参数会被拒绝。
+ */
+ @Test
+ @DisplayName("拒绝包含非法字符的排序表达式")
+ void escapeOrderBySqlShouldRejectUnsafeCharacters() {
+ assertThrows(IllegalArgumentException.class,
+ () -> SqlUtil.escapeOrderBySql("create_time desc;drop table sys_user"));
+ }
+
+ /**
+ * 验证单引号、关键字及大小写空白变体无法绕过过滤。
+ */
+ @Test
+ @DisplayName("拒绝单引号和 SQL 敏感关键词")
+ void filterKeywordShouldRejectRiskyInput() {
+ assertThrows(UtilException.class, () -> SqlUtil.filterKeyword("name='admin'"));
+ assertThrows(UtilException.class, () -> SqlUtil.filterKeyword("union select password"));
+ assertThrows(UtilException.class, () -> SqlUtil.filterKeyword("UNION\tSELECT password"));
+ }
+
+ /**
+ * 验证普通业务文本和空值不会被误判为 SQL 注入。
+ */
+ @Test
+ @DisplayName("允许普通业务查询文本")
+ void filterKeywordShouldAcceptRegularText() {
+ assertDoesNotThrow(() -> SqlUtil.filterKeyword("normal-business-value"));
+ assertDoesNotThrow(() -> SqlUtil.filterKeyword(null));
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StreamUtilsTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StreamUtilsTest.java
new file mode 100644
index 000000000..bbfe4dd46
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StreamUtilsTest.java
@@ -0,0 +1,102 @@
+package org.dromara.common.core.utils;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.Arrays;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("StreamUtils 单元测试")
+class StreamUtilsTest {
+
+ /**
+ * 验证过滤结果保持可修改,并对空输入返回空集合。
+ */
+ @Test
+ @DisplayName("过滤结果可修改且空集合返回空列表")
+ void filterShouldReturnMutableListAndHandleEmptyInput() {
+ List result = StreamUtils.filter(List.of(1, 2, 3), value -> value % 2 == 1);
+
+ assertEquals(List.of(1, 3), result);
+ assertDoesNotThrow(() -> result.add(5));
+ assertTrue(StreamUtils.filter(null, value -> true).isEmpty());
+ }
+
+ /**
+ * 验证集合拼接会跳过映射函数产生的空值。
+ */
+ @Test
+ @DisplayName("拼接时忽略映射结果中的空值")
+ void joinShouldIgnoreNullMappedValues() {
+ String result = StreamUtils.join(List.of("first", "skip", "last"),
+ value -> "skip".equals(value) ? null : value, "|");
+
+ assertEquals("first|last", result);
+ assertEquals(StringUtils.EMPTY, StreamUtils.join(List.of(), value -> value));
+ }
+
+ /**
+ * 验证集合转 Map 时的空元素过滤和重复键处理规则。
+ */
+ @Test
+ @DisplayName("转 Map 时重复键保留第一个值并忽略空元素")
+ void toMapShouldKeepFirstDuplicateValue() {
+ List items = Arrays.asList(
+ new TestItem(1L, "first"),
+ new TestItem(1L, "second"),
+ null,
+ new TestItem(2L, "third")
+ );
+
+ Map result = StreamUtils.toMap(items, TestItem::id, TestItem::value);
+
+ assertEquals(Map.of(1L, "first", 2L, "third"), result);
+ }
+
+ /**
+ * 验证分组结果按照键在输入中的首次出现顺序排列。
+ */
+ @Test
+ @DisplayName("分组结果保持输入键的出现顺序")
+ void groupByKeyShouldPreserveKeyOrder() {
+ List items = List.of(
+ new TestItem(2L, "a"),
+ new TestItem(1L, "b"),
+ new TestItem(2L, "c")
+ );
+
+ Map> result = StreamUtils.groupByKey(items, TestItem::id);
+
+ assertEquals(List.of(2L, 1L), result.keySet().stream().toList());
+ assertEquals(List.of("a", "c"), result.get(2L).stream().map(TestItem::value).toList());
+ }
+
+ /**
+ * 验证两个 Map 合并时同时处理独有键和共有键。
+ */
+ @Test
+ @DisplayName("合并 Map 时覆盖两侧独有键和共有键")
+ void mergeShouldCoverUnionOfKeys() {
+ Map left = new LinkedHashMap<>();
+ left.put(1L, "L1");
+ left.put(2L, "L2");
+ Map right = new LinkedHashMap<>();
+ right.put(2L, "R2");
+ right.put(3L, "R3");
+
+ Map result = StreamUtils.merge(left, right,
+ (leftValue, rightValue) -> String.valueOf(leftValue) + ":" + String.valueOf(rightValue));
+
+ assertEquals("L1:null", result.get(1L));
+ assertEquals("L2:R2", result.get(2L));
+ assertEquals("null:R3", result.get(3L));
+ }
+
+ private record TestItem(Long id, String value) {
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StringUtilsTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StringUtilsTest.java
new file mode 100644
index 000000000..4521a396d
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/StringUtilsTest.java
@@ -0,0 +1,100 @@
+package org.dromara.common.core.utils;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.nio.charset.StandardCharsets;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("StringUtils 补充单元测试")
+class StringUtilsTest {
+
+ /**
+ * 验证空白默认值、裁剪、截取、格式化和命名转换等基础字符串操作。
+ */
+ @Test
+ @DisplayName("处理基础字符串转换")
+ void shouldHandleBasicStringTransformations() {
+ assertEquals("fallback", StringUtils.blankToDefault(" ", "fallback"));
+ assertTrue(StringUtils.isEmpty(""));
+ assertTrue(StringUtils.isNotEmpty("value"));
+ assertEquals("value", StringUtils.trim(" value "));
+ assertEquals("bc", StringUtils.substring("abcd", 1, 3));
+ assertEquals("id=12", StringUtils.format("id={}", 12));
+ assertEquals("user_name", StringUtils.toUnderScoreCase("userName"));
+ assertEquals("HelloWorld", StringUtils.convertToCamelCase("HELLO_WORLD"));
+ assertEquals("userName", StringUtils.toCamelCase("user_name"));
+ }
+
+ /**
+ * 验证分隔字符串时的空白过滤、裁剪、去重和自定义类型转换行为。
+ */
+ @Test
+ @DisplayName("拆分字符串集合")
+ void shouldSplitStringsIntoCollections() {
+ assertEquals(List.of("a", "b"), StringUtils.str2List(" a, ,b ", ",", true, true));
+ assertEquals(Set.of("a", "b"), StringUtils.str2Set("a,b,a", ","));
+ assertEquals(List.of("a", "b"), StringUtils.splitList("a,b"));
+ assertEquals(List.of(1, 2), StringUtils.splitTo("1|2", "|", value -> Integer.valueOf(value.toString())));
+ assertTrue(StringUtils.splitList(" ").isEmpty());
+ }
+
+ /**
+ * 验证 Ant 风格路径规则可以区分单层通配符、跨层通配符和空输入。
+ */
+ @Test
+ @DisplayName("匹配路径规则")
+ void shouldMatchAntStylePaths() {
+ assertTrue(StringUtils.isMatch("/system/**", "/system/user/list"));
+ assertFalse(StringUtils.isMatch("/system/*", "/system/user/list"));
+ assertTrue(StringUtils.matches("/system/user/list", List.of("/login", "/system/**")));
+ assertFalse(StringUtils.matches("", List.of("/**")));
+ assertFalse(StringUtils.matches("/system", List.of()));
+ }
+
+ /**
+ * 验证定长左补齐会补零、截取尾部,并正确处理空值。
+ */
+ @Test
+ @DisplayName("定长左补齐字符串")
+ void shouldPadOrTruncateFromLeft() {
+ assertEquals("0012", StringUtils.padl(12, 4));
+ assertEquals("cdef", StringUtils.padl("abcdef", 4, '0'));
+ assertEquals("***a", StringUtils.padl("a", 4, '*'));
+ assertEquals("000", StringUtils.padl(null, 3, '0'));
+ }
+
+ /**
+ * 验证大小写敏感和忽略大小写的查找、前后缀与替换方法保持不同语义。
+ */
+ @Test
+ @DisplayName("比较和替换字符串")
+ void shouldCompareAndReplaceStrings() {
+ assertTrue(StringUtils.containsAnyIgnoreCase("Hello", "WORLD", "he"));
+ assertTrue(StringUtils.inStringIgnoreCase("ADMIN", "user", "admin"));
+ assertTrue(StringUtils.startWithAnyIgnoreCase("Bearer token", "basic", "bearer"));
+ assertTrue(StringUtils.equalsAny("a", "b", "a"));
+ assertTrue(StringUtils.equalsAnyIgnoreCase("A", "b", "a"));
+ assertTrue(StringUtils.containsIgnoreCase("Hello", "ELL"));
+ assertTrue(StringUtils.endsWithIgnoreCase("report.XLSX", ".xlsx"));
+ assertEquals(2, StringUtils.indexOf("abcabc", "ca"));
+ assertEquals("path", StringUtils.removeStart("/path", "/"));
+ assertEquals("a-b_c", StringUtils.replaceOnce("a_b_c", "_", "-"));
+ }
+
+ /**
+ * 验证字符集转换、URL 判断与逗号拼接的公共便捷方法。
+ */
+ @Test
+ @DisplayName("处理编码和拼接")
+ void shouldHandleEncodingUrlAndJoining() {
+ assertEquals("中文", StringUtils.convert("中文", StandardCharsets.UTF_8, StandardCharsets.UTF_8));
+ assertEquals("", StringUtils.convert("", StandardCharsets.UTF_8, StandardCharsets.UTF_16));
+ assertTrue(StringUtils.ishttp("https://example.com/path"));
+ assertEquals("a,b", StringUtils.joinComma(List.of("a", "b")));
+ assertEquals("1,2", StringUtils.joinComma(new Integer[]{1, 2}));
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/TreeBuildUtilsTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/TreeBuildUtilsTest.java
new file mode 100644
index 000000000..ba65ba69b
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/utils/TreeBuildUtilsTest.java
@@ -0,0 +1,98 @@
+package org.dromara.common.core.utils;
+
+import cn.hutool.core.lang.tree.Tree;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisplayName("TreeBuildUtils 单元测试")
+class TreeBuildUtilsTest {
+
+ static {
+ // TreeBuildUtils 会将 Hutool 默认名称键调整为前端使用的 label。
+ TreeBuildUtils.DEFAULT_CONFIG.getNameKey();
+ }
+
+ /**
+ * 验证递归遍历只返回没有子节点的叶子节点。
+ */
+ @Test
+ @DisplayName("获取树中所有叶子节点")
+ void getLeafNodesShouldReturnOnlyLeaves() {
+ Tree root = tree(1L, "root");
+ Tree leaf = tree(2L, "leaf");
+ Tree branch = tree(3L, "branch");
+ Tree nestedLeaf = tree(4L, "nested");
+ branch.setChildren(List.of(nestedLeaf));
+ root.setChildren(List.of(leaf, branch));
+
+ List> result = TreeBuildUtils.getLeafNodes(List.of(root));
+
+ assertEquals(List.of(2L, 4L), result.stream().map(Tree::getId).toList());
+ assertTrue(TreeBuildUtils.getLeafNodes(null).isEmpty());
+ }
+
+ /**
+ * 验证树节点按照深度优先顺序生成完整路径键。
+ */
+ @Test
+ @DisplayName("按深度优先顺序构建节点路径映射")
+ void buildTreeNodeMapShouldCreateFullPaths() {
+ Tree root = tree(1L, "root");
+ Tree child = tree(2L, "child");
+ Tree leaf = tree(3L, "leaf");
+ child.setChildren(List.of(leaf));
+ root.setChildren(List.of(child));
+
+ Map> result = TreeBuildUtils.buildTreeNodeMap(List.of(root), "/", Tree::getName);
+
+ assertEquals(List.of("root", "root/child", "root/child/leaf"), result.keySet().stream().toList());
+ assertEquals(3L, result.get("root/child/leaf").getId());
+ }
+
+ /**
+ * 验证不同父级来源的顶级节点可以合并为多根树。
+ */
+ @Test
+ @DisplayName("构建包含多个顶级节点的树")
+ void buildMultiRootShouldKeepAllRoots() {
+ List nodes = List.of(
+ new TestNode(1L, 0L, "root-a"),
+ new TestNode(2L, 1L, "child-a"),
+ new TestNode(10L, 9L, "root-b")
+ );
+
+ List> result = TreeBuildUtils.buildMultiRoot(nodes, TestNode::id, TestNode::parentId,
+ (node, treeNode) -> treeNode.setId(node.id()).setParentId(node.parentId()).setName(node.name()));
+
+ Set rootIds = result.stream().map(Tree::getId).collect(Collectors.toSet());
+ assertEquals(Set.of(1L, 10L), rootIds);
+ Tree firstRoot = result.stream().filter(tree -> tree.getId().equals(1L)).findFirst().orElseThrow();
+ assertEquals(List.of(2L), firstRoot.getChildren().stream().map(Tree::getId).toList());
+ }
+
+ /**
+ * 创建测试使用的最小树节点。
+ *
+ * @param id 节点 ID
+ * @param name 节点名称
+ * @return 树节点
+ */
+ private static Tree tree(Long id, String name) {
+ Tree tree = new Tree<>();
+ tree.setId(id);
+ tree.setName(name);
+ return tree;
+ }
+
+ private record TestNode(Long id, Long parentId, String name) {
+ }
+
+}
diff --git a/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/validate/enums/EnumPatternValidatorTest.java b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/validate/enums/EnumPatternValidatorTest.java
new file mode 100644
index 000000000..887d52280
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-core/src/test/java/org/dromara/common/core/validate/enums/EnumPatternValidatorTest.java
@@ -0,0 +1,85 @@
+package org.dromara.common.core.validate.enums;
+
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisplayName("EnumPatternValidator 单元测试")
+class EnumPatternValidatorTest {
+
+ /**
+ * 验证字符串形式的数字能够按枚举字段实际类型转换并完成合法值校验。
+ */
+ @Test
+ @DisplayName("校验枚举字段值")
+ void shouldValidateConvertedEnumFieldValues() throws NoSuchFieldException {
+ EnumPatternValidator validator = validatorFor("status");
+
+ assertTrue(validator.isValid(null, null));
+ assertTrue(validator.isValid(1, null));
+ assertTrue(validator.isValid("2", null));
+ assertFalse(validator.isValid("invalid", null));
+ assertFalse(validator.isValid(9, null));
+ }
+
+ /**
+ * 验证未配置枚举字段名时不会错误接受非空输入。
+ */
+ @Test
+ @DisplayName("拒绝缺少枚举字段配置的输入")
+ void shouldRejectValueWhenEnumFieldIsNotConfigured() throws NoSuchFieldException {
+ EnumPatternValidator validator = validatorFor("unconfigured");
+
+ assertTrue(validator.isValid(null, null));
+ assertFalse(validator.isValid(1, null));
+ }
+
+ /**
+ * 根据测试字段上的真实注解创建并初始化枚举校验器。
+ *
+ * @param fieldName 测试字段名
+ * @return 已初始化的校验器
+ */
+ private static EnumPatternValidator validatorFor(String fieldName) throws NoSuchFieldException {
+ Field field = ValidationTarget.class.getDeclaredField(fieldName);
+ EnumPatternValidator validator = new EnumPatternValidator();
+ validator.initialize(field.getAnnotation(EnumPattern.class));
+ return validator;
+ }
+
+ private enum Status {
+ ENABLED(1), DISABLED(2);
+
+ private final Integer code;
+
+ /**
+ * 创建带校验码的测试状态。
+ *
+ * @param code 枚举校验码
+ */
+ Status(Integer code) {
+ this.code = code;
+ }
+
+ /**
+ * 返回供枚举校验器读取的测试状态码。
+ *
+ * @return 状态码
+ */
+ public Integer getCode() {
+ return code;
+ }
+ }
+
+ private static class ValidationTarget {
+ @EnumPattern(type = Status.class, fieldName = "code")
+ private Integer status;
+
+ @EnumPattern(type = Status.class, fieldName = "")
+ private Integer unconfigured;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-doc/src/test/java/org/dromara/common/doc/SaTokenSecurityMetadataTest.java b/ruoyi-common/ruoyi-common-doc/src/test/java/org/dromara/common/doc/SaTokenSecurityMetadataTest.java
new file mode 100644
index 000000000..2bfb41ee2
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-doc/src/test/java/org/dromara/common/doc/SaTokenSecurityMetadataTest.java
@@ -0,0 +1,45 @@
+package org.dromara.common.doc;
+
+import org.dromara.common.doc.core.model.SaTokenSecurityMetadata;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisplayName("common-doc 功能单元测试")
+class SaTokenSecurityMetadataTest {
+
+ /**
+ * 验证未声明权限和忽略权限时分别输出登录要求与忽略检查说明。
+ */
+ @Test
+ @DisplayName("生成基础权限文档")
+ void shouldDescribeLoginAndIgnoredSecurity() {
+ SaTokenSecurityMetadata metadata = new SaTokenSecurityMetadata();
+ assertTrue(metadata.toMarkdownString().contains("需要登录"));
+
+ metadata.setIgnore(true);
+ assertTrue(metadata.toMarkdownString().contains("忽略权限检查"));
+ }
+
+ /**
+ * 验证权限、或角色和角色校验能够按 AND/OR 模式生成可读的 Markdown。
+ */
+ @Test
+ @DisplayName("生成权限和角色文档")
+ void shouldDescribePermissionsAndRoles() {
+ SaTokenSecurityMetadata metadata = new SaTokenSecurityMetadata();
+ metadata.addPermission(new String[]{"system:user:list", "system:user:query"}, "AND", "permission",
+ new String[]{"admin", "auditor"});
+ metadata.addRole(new String[]{"manager", "operator"}, "OR", "role");
+
+ String markdown = metadata.toMarkdownString();
+
+ assertTrue(markdown.contains("`system:user:list` & `system:user:query`"));
+ assertTrue(markdown.contains("或角色:`admin` & `auditor`"));
+ assertTrue(markdown.contains("`manager` | `operator`"));
+ assertEquals(1, metadata.getPermissions().size());
+ assertEquals(1, metadata.getRoles().size());
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-elasticsearch/src/test/java/org/dromara/common/elasticsearch/ActuatorEnvironmentPostProcessorTest.java b/ruoyi-common/ruoyi-common-elasticsearch/src/test/java/org/dromara/common/elasticsearch/ActuatorEnvironmentPostProcessorTest.java
new file mode 100644
index 000000000..b46a9bd63
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-elasticsearch/src/test/java/org/dromara/common/elasticsearch/ActuatorEnvironmentPostProcessorTest.java
@@ -0,0 +1,48 @@
+package org.dromara.common.elasticsearch;
+
+import org.dromara.common.elasticsearch.config.ActuatorEnvironmentPostProcessor;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.core.Ordered;
+import org.springframework.mock.env.MockEnvironment;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+@DisplayName("common-elasticsearch 功能单元测试")
+class ActuatorEnvironmentPostProcessorTest {
+
+ /**
+ * 清理测试写入的 JVM 系统属性,避免影响同一测试进程中的其他用例。
+ */
+ @AfterEach
+ void clearHealthProperty() {
+ System.clearProperty("management.health.elasticsearch.enabled");
+ }
+
+ /**
+ * 验证 Easy-ES 开关会同步到 Elasticsearch 健康检查,并以最高优先级执行。
+ */
+ @Test
+ @DisplayName("同步 Elasticsearch 健康检查开关")
+ void shouldSynchronizeElasticsearchHealthFlag() {
+ ActuatorEnvironmentPostProcessor processor = new ActuatorEnvironmentPostProcessor();
+ MockEnvironment environment = new MockEnvironment().withProperty("easy-es.enable", "true");
+
+ processor.postProcessEnvironment(environment, null);
+
+ assertEquals("true", System.getProperty("management.health.elasticsearch.enabled"));
+ assertEquals(Ordered.HIGHEST_PRECEDENCE, processor.getOrder());
+ }
+
+ /**
+ * 验证缺少 Easy-ES 配置时健康检查默认关闭,避免未配置连接时触发探测。
+ */
+ @Test
+ @DisplayName("默认关闭 Elasticsearch 健康检查")
+ void shouldDisableElasticsearchHealthByDefault() {
+ new ActuatorEnvironmentPostProcessor().postProcessEnvironment(new MockEnvironment(), null);
+
+ assertEquals("false", System.getProperty("management.health.elasticsearch.enabled"));
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptFunctionTest.java b/ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptFunctionTest.java
new file mode 100644
index 000000000..ce329c3e9
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptFunctionTest.java
@@ -0,0 +1,111 @@
+package org.dromara.common.encrypt;
+
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.encrypt.annotation.EncryptField;
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.core.EncryptContextFactory;
+import org.dromara.common.encrypt.core.EncryptorManager;
+import org.dromara.common.encrypt.enums.AlgorithmType;
+import org.dromara.common.encrypt.enums.EncodeType;
+import org.dromara.common.encrypt.properties.EncryptorProperties;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+
+import java.lang.reflect.Field;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.*;
+
+@DisplayName("common-encrypt 功能单元测试")
+class EncryptFunctionTest {
+
+ /**
+ * 验证 Base64、AES 和 SM4 的常用加解密能够无损往返。
+ */
+ @Test
+ @DisplayName("常用对称算法加解密往返")
+ void shouldRoundTripCommonSymmetricAlgorithms() {
+ String text = "RuoYi-Vue-Plus";
+ String aesKey = "1234567890abcdef";
+ String sm4Key = "abcdef1234567890";
+
+ assertEquals(text, EncryptUtils.decryptByBase64(EncryptUtils.encryptByBase64(text)));
+ assertEquals(text, EncryptUtils.decryptByAes(EncryptUtils.encryptByAes(text, aesKey), aesKey));
+ assertEquals(text, EncryptUtils.decryptBySm4(EncryptUtils.encryptBySm4(text, sm4Key), sm4Key));
+ }
+
+ /**
+ * 验证非法对称密钥长度在执行加密前被拒绝。
+ */
+ @Test
+ @DisplayName("校验对称算法密钥长度")
+ void shouldRejectInvalidSymmetricKeys() {
+ assertThrows(IllegalArgumentException.class, () -> EncryptUtils.encryptByAes("data", "short"));
+ assertThrows(IllegalArgumentException.class, () -> EncryptUtils.encryptBySm4("data", "short"));
+ }
+
+ /**
+ * 验证生成的 RSA 密钥满足校验要求并可以完成公钥加密、私钥解密。
+ */
+ @Test
+ @DisplayName("RSA 密钥生成和加解密往返")
+ void shouldGenerateAndUseRsaKeys() {
+ Map keys = EncryptUtils.generateRsaKey();
+
+ assertDoesNotThrow(() -> EncryptUtils.validateRsaPublicKey(keys.get(EncryptUtils.PUBLIC_KEY)));
+ assertDoesNotThrow(() -> EncryptUtils.validateRsaPrivateKey(keys.get(EncryptUtils.PRIVATE_KEY)));
+ String encrypted = EncryptUtils.encryptByRsa("secure-data", keys.get(EncryptUtils.PUBLIC_KEY));
+ assertEquals("secure-data", EncryptUtils.decryptByRsa(encrypted, keys.get(EncryptUtils.PRIVATE_KEY)));
+ }
+
+ /**
+ * 验证字段注解优先于默认配置构建加密上下文。
+ *
+ * @throws Exception 读取测试字段失败
+ */
+ @Test
+ @DisplayName("合并字段注解和默认加密配置")
+ void shouldCreateEncryptContextFromAnnotationAndDefaults() throws Exception {
+ EncryptorProperties properties = new EncryptorProperties();
+ properties.setAlgorithm(AlgorithmType.AES);
+ properties.setEncode(EncodeType.BASE64);
+ properties.setPassword("default-password");
+ properties.setPublicKey("default-public");
+ properties.setPrivateKey("default-private");
+ Field field = TestEntity.class.getDeclaredField("secret");
+
+ EncryptContext context = new EncryptContextFactory(properties).create(field);
+
+ assertEquals(AlgorithmType.SM4, context.getAlgorithm());
+ assertEquals(EncodeType.HEX, context.getEncode());
+ assertEquals("field-password", context.getPassword());
+ assertEquals("default-public", context.getPublicKey());
+ assertEquals("default-private", context.getPrivateKey());
+ }
+
+ /**
+ * 验证加密管理器添加统一密文头,并避免对已有密文重复加密。
+ */
+ @Test
+ @DisplayName("管理带标识头的加密值")
+ void shouldManageEncryptedValueHeader() {
+ EncryptContext context = new EncryptContext();
+ context.setAlgorithm(AlgorithmType.BASE64);
+ context.setEncode(EncodeType.BASE64);
+ EncryptorManager manager = new EncryptorManager("");
+
+ String encrypted = manager.encrypt("plain-text", context);
+
+ assertTrue(encrypted.startsWith(Constants.ENCRYPT_HEADER));
+ assertEquals(encrypted, manager.encrypt(encrypted, context));
+ assertEquals("plain-text", manager.decrypt(encrypted, context));
+ assertEquals("plain-text", manager.decrypt("plain-text", context));
+ }
+
+ private static class TestEntity {
+
+ @EncryptField(algorithm = AlgorithmType.SM4, encode = EncodeType.HEX, password = "field-password")
+ private String secret;
+ }
+}
diff --git a/ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptInfrastructureTest.java b/ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptInfrastructureTest.java
new file mode 100644
index 000000000..d0f162d79
--- /dev/null
+++ b/ruoyi-common/ruoyi-common-encrypt/src/test/java/org/dromara/common/encrypt/EncryptInfrastructureTest.java
@@ -0,0 +1,156 @@
+package org.dromara.common.encrypt;
+
+import org.dromara.common.core.constant.Constants;
+import org.dromara.common.encrypt.annotation.EncryptField;
+import org.dromara.common.encrypt.core.EncryptContext;
+import org.dromara.common.encrypt.core.EncryptContextFactory;
+import org.dromara.common.encrypt.core.EncryptedFieldProcessor;
+import org.dromara.common.encrypt.core.EncryptorManager;
+import org.dromara.common.encrypt.core.IEncryptor;
+import org.dromara.common.encrypt.enums.AlgorithmType;
+import org.dromara.common.encrypt.enums.EncodeType;
+import org.dromara.common.encrypt.filter.DecryptRequestBodyWrapper;
+import org.dromara.common.encrypt.filter.EncryptResponseBodyWrapper;
+import org.dromara.common.encrypt.properties.EncryptorProperties;
+import org.dromara.common.encrypt.utils.EncryptUtils;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+import org.springframework.mock.web.MockHttpServletResponse;
+
+import java.nio.charset.StandardCharsets;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotSame;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+@DisplayName("common-encrypt 基础设施单元测试")
+class EncryptInfrastructureTest {
+
+ /**
+ * 验证集合和 Map 中的加密字段会被处理,且字段快照能够恢复持久化前的原始值。
+ */
+ @Test
+ @DisplayName("加密并恢复容器中的字段")
+ void shouldEncryptAndRestoreFieldsInsideContainers() {
+ EncryptorProperties properties = new EncryptorProperties();
+ properties.setAlgorithm(AlgorithmType.BASE64);
+ properties.setEncode(EncodeType.BASE64);
+ EncryptorManager manager = new EncryptorManager("");
+ EncryptedFieldProcessor processor = new EncryptedFieldProcessor(
+ manager, new EncryptContextFactory(properties));
+ TestSecretEntity first = new TestSecretEntity("first");
+ TestSecretEntity second = new TestSecretEntity("second");
+ List