diff --git a/src/test/java/com/yexuejc/base/encrypt/AESEnhancedTest.java b/src/test/java/com/yexuejc/base/encrypt/AESEnhancedTest.java new file mode 100644 index 0000000..159a60b --- /dev/null +++ b/src/test/java/com/yexuejc/base/encrypt/AESEnhancedTest.java @@ -0,0 +1,232 @@ +package com.yexuejc.base.encrypt; + +import com.yexuejc.base.exception.BaseException; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Assertions; + +import java.nio.charset.StandardCharsets; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * AES加密增强测试类 - 验证优化后的安全性和功能 + * + * @author optimization + * @date 2025/09/23 + */ +@DisplayName("AES加密增强测试") +class AESEnhancedTest { + + private AES aes; + + @BeforeEach + void setUp() { + aes = AES.builder(); + } + + @Test + @DisplayName("测试密钥和IV必须设置") + void testKeyAndIvRequired() { + // 测试未设置密钥时抛出异常 + IllegalStateException exception = assertThrows(IllegalStateException.class, () -> { + aes.encrypt("test data"); + }); + assertTrue(exception.getMessage().contains("AES密钥未设置")); + } + + @Test + @DisplayName("测试密钥长度验证") + void testKeyLengthValidation() { + // 测试无效密钥长度 + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + aes.setKey("short"); + }); + assertTrue(exception.getMessage().contains("AES密钥长度必须是16、24或32字节")); + + // 测试有效密钥长度 + assertDoesNotThrow(() -> { + aes.setKey("1234567890123456"); // 16字节 + aes.setKey("123456789012345678901234"); // 24字节 + aes.setKey("12345678901234567890123456789012"); // 32字节 + }); + } + + @Test + @DisplayName("测试IV长度验证") + void testIvLengthValidation() { + // 设置有效密钥 + aes.setKey("1234567890123456"); + + // 测试无效IV长度 + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + aes.setIv("short"); + }); + assertTrue(exception.getMessage().contains("AES初始向量长度必须是16字节")); + + // 测试有效IV长度 + assertDoesNotThrow(() -> { + aes.setIv("1234567890123456"); // 16字节 + }); + } + + @Test + @DisplayName("测试输入参数验证") + void testInputValidation() { + aes.setKey("1234567890123456") + .setIv("1234567890123456") + .setAlgorithm(AES.ALGORITHM.AES_CBC_PKCS5Padding); + + // 测试空输入 + IllegalArgumentException encryptException = assertThrows(IllegalArgumentException.class, () -> { + aes.encrypt(null); + }); + assertTrue(encryptException.getMessage().contains("加密数据不能为空")); + + IllegalArgumentException decryptException = assertThrows(IllegalArgumentException.class, () -> { + aes.decrypt(null); + }); + assertTrue(decryptException.getMessage().contains("解密数据不能为空")); + } + + @Test + @DisplayName("测试字符集设置验证") + void testCharsetValidation() { + IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> { + aes.setCharset(null); + }); + assertTrue(exception.getMessage().contains("字符集不能为空")); + + // 测试有效字符集设置 + assertDoesNotThrow(() -> { + aes.setCharset(StandardCharsets.UTF_8); + }); + } + + @Test + @DisplayName("测试ECB模式不需要IV") + void testECBModeWithoutIV() throws BaseException { + String data = "Hello World!"; + aes.setKey("1234567890123456") + .setAlgorithm(AES.ALGORITHM.AES_ECB_PKCS5Padding); + + // ECB模式不需要IV + String encrypted = aes.encrypt(data); + assertNotNull(encrypted); + assertFalse(encrypted.isEmpty()); + + String decrypted = aes.decrypt(encrypted); + assertEquals(data, decrypted); + } + + @Test + @DisplayName("测试完整的加密解密流程") + void testCompleteEncryptDecryptFlow() throws BaseException { + String originalData = "这是一个测试字符串包含中文!"; + + aes.setKey("1234567890123456") + .setIv("1234567890123456") + .setAlgorithm(AES.ALGORITHM.AES_CBC_PKCS5Padding) + .setCharset(StandardCharsets.UTF_8); + + String encrypted = aes.encrypt(originalData); + assertNotNull(encrypted); + assertNotEquals(originalData, encrypted); + + String decrypted = aes.decrypt(encrypted); + assertEquals(originalData, decrypted); + } + + @Test + @DisplayName("测试不同AES算法模式") + void testDifferentAlgorithms() throws BaseException { + String data = "Test Data"; + String key = "1234567890123456"; + String iv = "1234567890123456"; + + AES.ALGORITHM[] algorithms = { + AES.ALGORITHM.AES_CBC_PKCS5Padding, + AES.ALGORITHM.AES_OFB_PKCS5Padding, + AES.ALGORITHM.AES_ECB_PKCS5Padding + }; + + for (AES.ALGORITHM algorithm : algorithms) { + AES testAes = AES.builder() + .setKey(key) + .setAlgorithm(algorithm); + + if (!algorithm.code.contains("ECB")) { + testAes.setIv(iv); + } + + String encrypted = testAes.encrypt(data); + String decrypted = testAes.decrypt(encrypted); + + assertEquals(data, decrypted, "Algorithm " + algorithm.code + " failed"); + } + } + + @Test + @DisplayName("测试建造者模式") + void testBuilderPattern() { + AES aes1 = AES.builder(); + AES aes2 = AES.builder(); + + // 现在返回不同的实例,避免状态共享 + assertNotNull(aes1); + assertNotNull(aes2); + assertNotSame(aes1, aes2); // 确认不是单例 + } + + @Test + @DisplayName("测试解密异常处理") + void testDecryptErrorHandling() { + aes.setKey("1234567890123456") + .setIv("1234567890123456") + .setAlgorithm(AES.ALGORITHM.AES_CBC_PKCS5Padding); + + // 测试无效Base64数据 + BaseException exception = assertThrows(BaseException.class, () -> { + aes.decrypt("invalid_base64_data"); + }); + assertNotNull(exception.getErrorCode()); + } + + @Test + @DisplayName("测试不同密钥长度") + void testDifferentKeyLengths() throws BaseException { + String data = "Test Data"; + String iv = "1234567890123456"; + + // 测试16字节密钥 + AES aes16 = AES.builder() + .setKey("1234567890123456") + .setIv(iv) + .setAlgorithm(AES.ALGORITHM.AES_CBC_PKCS5Padding); + + String encrypted16 = aes16.encrypt(data); + String decrypted16 = aes16.decrypt(encrypted16); + assertEquals(data, decrypted16); + + // 测试24字节密钥 + AES aes24 = AES.builder() + .setKey("123456789012345678901234") + .setIv(iv) + .setAlgorithm(AES.ALGORITHM.AES_CBC_PKCS5Padding); + + String encrypted24 = aes24.encrypt(data); + String decrypted24 = aes24.decrypt(encrypted24); + assertEquals(data, decrypted24); + + // 测试32字节密钥 + AES aes32 = AES.builder() + .setKey("12345678901234567890123456789012") + .setIv(iv) + .setAlgorithm(AES.ALGORITHM.AES_CBC_PKCS5Padding); + + String encrypted32 = aes32.encrypt(data); + String decrypted32 = aes32.decrypt(encrypted32); + assertEquals(data, decrypted32); + } +} \ No newline at end of file diff --git a/src/test/java/com/yexuejc/base/util/ObjUtilEnhancedTest.java b/src/test/java/com/yexuejc/base/util/ObjUtilEnhancedTest.java new file mode 100644 index 0000000..4f26211 --- /dev/null +++ b/src/test/java/com/yexuejc/base/util/ObjUtilEnhancedTest.java @@ -0,0 +1,237 @@ +package com.yexuejc.base.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; + +import java.util.Arrays; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * ObjUtil增强测试类 - 验证优化后的异常处理和功能 + * + * @author optimization + * @date 2025/09/23 + */ +@DisplayName("ObjUtil增强测试") +class ObjUtilEnhancedTest { + + @Test + @DisplayName("测试基本字段复制功能") + void testBasicFieldCopy() throws Exception { + SourceObject source = new SourceObject(); + source.setName("Test Name"); + source.setAge(25); + source.setEmail("test@example.com"); + + TargetObject target = ObjUtil.copy(source, TargetObject.class, null, null); + + assertNotNull(target); + assertEquals(source.getName(), target.getName()); + assertEquals(source.getAge(), target.getAge()); + assertEquals(source.getEmail(), target.getEmail()); + } + + @Test + @DisplayName("测试包含字段过滤") + void testIncludeFieldFiltering() throws Exception { + SourceObject source = new SourceObject(); + source.setName("Test Name"); + source.setAge(25); + source.setEmail("test@example.com"); + + List includeFields = Arrays.asList("name", "age"); + TargetObject target = ObjUtil.copy(source, TargetObject.class, includeFields, null); + + assertNotNull(target); + assertEquals(source.getName(), target.getName()); + assertEquals(source.getAge(), target.getAge()); + assertNull(target.getEmail()); // 不在包含列表中 + } + + @Test + @DisplayName("测试排除字段过滤") + void testExcludeFieldFiltering() throws Exception { + SourceObject source = new SourceObject(); + source.setName("Test Name"); + source.setAge(25); + source.setEmail("test@example.com"); + + List excludeFields = Arrays.asList("email"); + TargetObject target = ObjUtil.copy(source, TargetObject.class, null, excludeFields); + + assertNotNull(target); + assertEquals(source.getName(), target.getName()); + assertEquals(source.getAge(), target.getAge()); + assertNull(target.getEmail()); // 被排除 + } + + @Test + @DisplayName("测试使用Setter方法复制") + void testCopyWithSetter() throws Exception { + SourceObject source = new SourceObject(); + source.setName("Test Name"); + source.setAge(25); + + TargetObjectWithSetter target = ObjUtil.copy(source, TargetObjectWithSetter.class, true); + + assertNotNull(target); + assertEquals(source.getName(), target.getName()); + assertEquals(source.getAge(), target.getAge()); + } + + @Test + @DisplayName("测试字段不匹配的处理") + void testFieldMismatchHandling() throws Exception { + SourceObjectWithExtraField source = new SourceObjectWithExtraField(); + source.setName("Test Name"); + source.setAge(25); + source.setExtraField("Extra Value"); + + // 目标类没有extraField字段 + TargetObject target = ObjUtil.copy(source, TargetObject.class, null, null); + + assertNotNull(target); + assertEquals(source.getName(), target.getName()); + assertEquals(source.getAge(), target.getAge()); + // extraField应该被忽略,不会导致异常 + } + + @Test + @DisplayName("测试空源对象处理") + void testNullSourceHandling() { + assertThrows(NullPointerException.class, () -> { + ObjUtil.copy(null, TargetObject.class, null, null); + }); + } + + @Test + @DisplayName("测试获取所有字段功能") + void testGetAllFields() { + List fields = ObjUtil.getAllFields(ChildObject.class); + + assertNotNull(fields); + assertTrue(fields.size() >= 3); // 至少包含child字段和父类的两个字段 + + // 验证包含父类字段 + boolean hasParentField1 = fields.stream().anyMatch(f -> "parentField1".equals(f.getName())); + boolean hasParentField2 = fields.stream().anyMatch(f -> "parentField2".equals(f.getName())); + boolean hasChildField = fields.stream().anyMatch(f -> "childField".equals(f.getName())); + + assertTrue(hasParentField1); + assertTrue(hasParentField2); + assertTrue(hasChildField); + } + + @Test + @DisplayName("测试获取所有Getter方法功能") + void testGetAllGetterMethods() { + List methods = ObjUtil.getAllGetterMethods(ChildObject.class, "get"); + + assertNotNull(methods); + assertTrue(methods.size() >= 3); // 至少包含三个getter方法 + + // 验证包含父类方法 + boolean hasGetParentField1 = methods.stream().anyMatch(m -> "getParentField1".equals(m.getName())); + boolean hasGetParentField2 = methods.stream().anyMatch(m -> "getParentField2".equals(m.getName())); + boolean hasGetChildField = methods.stream().anyMatch(m -> "getChildField".equals(m.getName())); + + assertTrue(hasGetParentField1); + assertTrue(hasGetParentField2); + assertTrue(hasGetChildField); + } + + @Test + @DisplayName("测试首字母小写工具方法") + void testLowerCaseFirstChar() { + assertEquals("test", ObjUtil.lowerCaseFirstChar("Test")); + assertEquals("tEST", ObjUtil.lowerCaseFirstChar("TEST")); + assertEquals("", ObjUtil.lowerCaseFirstChar("")); + assertNull(ObjUtil.lowerCaseFirstChar(null)); + assertEquals("a", ObjUtil.lowerCaseFirstChar("A")); + } + + @Test + @DisplayName("测试目标类没有默认构造函数的情况") + void testTargetClassWithoutDefaultConstructor() { + SourceObject source = new SourceObject(); + source.setName("Test"); + + assertThrows(Exception.class, () -> { + ObjUtil.copy(source, ClassWithoutDefaultConstructor.class, null, null); + }); + } + + // 测试用的内部类 + + static class SourceObject { + private String name; + private Integer age; + private String email; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Integer getAge() { return age; } + public void setAge(Integer age) { this.age = age; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + } + + static class SourceObjectWithExtraField extends SourceObject { + private String extraField; + + public String getExtraField() { return extraField; } + public void setExtraField(String extraField) { this.extraField = extraField; } + } + + static class TargetObject { + private String name; + private Integer age; + private String email; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Integer getAge() { return age; } + public void setAge(Integer age) { this.age = age; } + public String getEmail() { return email; } + public void setEmail(String email) { this.email = email; } + } + + static class TargetObjectWithSetter { + private String name; + private Integer age; + + public String getName() { return name; } + public void setName(String name) { this.name = name; } + public Integer getAge() { return age; } + public void setAge(Integer age) { this.age = age; } + } + + static class ParentObject { + private String parentField1; + private String parentField2; + + public String getParentField1() { return parentField1; } + public void setParentField1(String parentField1) { this.parentField1 = parentField1; } + public String getParentField2() { return parentField2; } + public void setParentField2(String parentField2) { this.parentField2 = parentField2; } + } + + static class ChildObject extends ParentObject { + private String childField; + + public String getChildField() { return childField; } + public void setChildField(String childField) { this.childField = childField; } + } + + static class ClassWithoutDefaultConstructor { + private String name; + + public ClassWithoutDefaultConstructor(String name) { + this.name = name; + } + + public String getName() { return name; } + } +} \ No newline at end of file diff --git a/src/test/java/com/yexuejc/base/util/StrUtilEnhancedTest.java b/src/test/java/com/yexuejc/base/util/StrUtilEnhancedTest.java new file mode 100644 index 0000000..7dd15f4 --- /dev/null +++ b/src/test/java/com/yexuejc/base/util/StrUtilEnhancedTest.java @@ -0,0 +1,288 @@ +package com.yexuejc.base.util; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.RepeatedTest; + +import java.util.HashSet; +import java.util.Set; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicBoolean; + +import static org.junit.jupiter.api.Assertions.*; + +/** + * StrUtil增强测试类 - 验证优化后的性能和线程安全性 + * + * @author optimization + * @date 2025/09/23 + */ +@DisplayName("StrUtil增强测试") +class StrUtilEnhancedTest { + + @Test + @DisplayName("测试MD5计算的一致性") + void testMD5Consistency() { + String input = "Hello World!"; + String expected = "ed076287532e86365e841e92bfc50d8c"; // 已知MD5值 + + String result1 = StrUtil.toMD5(input); + String result2 = StrUtil.toMD5(input); + + assertEquals(expected, result1); + assertEquals(result1, result2); + } + + @Test + @DisplayName("测试SHA256计算的一致性") + void testSHA256Consistency() { + String input = "Hello World!"; + String expected = "7f83b1657ff1fc53b92dc18148a1d65dfc2d4b1fa3d677284addd200126d9069"; // 已知SHA256值 + + String result1 = StrUtil.toSHA256(input); + String result2 = StrUtil.toSHA256(input); + + assertEquals(expected, result1); + assertEquals(result1, result2); + } + + @Test + @DisplayName("测试MD5处理null值") + void testMD5WithNull() { + assertNull(StrUtil.toMD5(null)); + } + + @Test + @DisplayName("测试SHA256处理null值") + void testSHA256WithNull() { + assertNull(StrUtil.toSHA256(null)); + } + + @Test + @DisplayName("测试不支持的SHA算法") + void testUnsupportedSHAAlgorithm() { + String result = StrUtil.toSHA("test", "INVALID_ALGORITHM"); + assertNull(result); + } + + @Test + @DisplayName("测试中文字符的MD5计算") + void testMD5WithChinese() { + String input = "你好世界"; + String result = StrUtil.toMD5(input); + + assertNotNull(result); + assertEquals(32, result.length()); // MD5长度应为32 + assertTrue(result.matches("[a-f0-9]+")); + } + + @Test + @DisplayName("测试中文字符的SHA256计算") + void testSHA256WithChinese() { + String input = "你好世界"; + String result = StrUtil.toSHA256(input); + + assertNotNull(result); + assertEquals(64, result.length()); // SHA256长度应为64 + assertTrue(result.matches("[a-f0-9]+")); + } + + @Test + @DisplayName("测试长字符串的MD5计算") + void testMD5WithLongString() { + StringBuilder longString = new StringBuilder(); + for (int i = 0; i < 10000; i++) { + longString.append("abcdefghij"); + } + + String result = StrUtil.toMD5(longString.toString()); + assertNotNull(result); + assertEquals(32, result.length()); + } + + @RepeatedTest(10) + @DisplayName("测试SecureRandom生成的随机性") + void testCodeIdRandomness() { + String id = "12345678901234567890123456789012"; // 32位ID + + String coded1 = StrUtil.codeId(id); + String coded2 = StrUtil.codeId(id); + + assertEquals(64, coded1.length()); + assertEquals(64, coded2.length()); + + // 由于使用SecureRandom,两次编码结果应该不同 + assertNotEquals(coded1, coded2); + + // 解码后应该得到原始ID + String decoded1 = StrUtil.decodeId(coded1); + String decoded2 = StrUtil.decodeId(coded2); + + assertEquals(id, decoded1); + assertEquals(id, decoded2); + } + + @Test + @DisplayName("测试codeId处理无效输入") + void testCodeIdWithInvalidInput() { + // 测试null + assertNull(StrUtil.codeId(null)); + + // 测试长度不是32的字符串 + String shortId = "short"; + assertEquals(shortId, StrUtil.codeId(shortId)); + + String longId = "this_is_a_very_long_string_that_exceeds_32_characters"; + assertEquals(longId, StrUtil.codeId(longId)); + } + + @Test + @DisplayName("测试decodeId处理无效输入") + void testDecodeIdWithInvalidInput() { + // 测试null + assertNull(StrUtil.decodeId(null)); + + // 测试长度不是64的字符串 + String shortCoded = "short"; + assertEquals(shortCoded, StrUtil.decodeId(shortCoded)); + + String longCoded = "this_is_a_very_long_string_that_exceeds_64_characters_and_should_be_returned_as_is"; + assertEquals(longCoded, StrUtil.decodeId(longCoded)); + } + + @Test + @DisplayName("测试线程安全性 - MD5计算") + void testMD5ThreadSafety() throws InterruptedException { + final int threadCount = 20; + final int iterationsPerThread = 100; + final String testString = "Thread Safety Test"; + final String expectedMD5 = StrUtil.toMD5(testString); + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + AtomicBoolean hasError = new AtomicBoolean(false); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + for (int j = 0; j < iterationsPerThread; j++) { + String result = StrUtil.toMD5(testString); + if (!expectedMD5.equals(result)) { + hasError.set(true); + break; + } + } + }); + } + + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + assertFalse(hasError.get(), "MD5计算在多线程环境下出现不一致结果"); + } + + @Test + @DisplayName("测试线程安全性 - SHA256计算") + void testSHA256ThreadSafety() throws InterruptedException { + final int threadCount = 20; + final int iterationsPerThread = 100; + final String testString = "Thread Safety Test"; + final String expectedSHA256 = StrUtil.toSHA256(testString); + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + AtomicBoolean hasError = new AtomicBoolean(false); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + for (int j = 0; j < iterationsPerThread; j++) { + String result = StrUtil.toSHA256(testString); + if (!expectedSHA256.equals(result)) { + hasError.set(true); + break; + } + } + }); + } + + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + assertFalse(hasError.get(), "SHA256计算在多线程环境下出现不一致结果"); + } + + @Test + @DisplayName("测试线程安全性 - codeId生成") + void testCodeIdThreadSafety() throws InterruptedException { + final int threadCount = 10; + final int iterationsPerThread = 50; + final String testId = "12345678901234567890123456789012"; + + ExecutorService executor = Executors.newFixedThreadPool(threadCount); + Set results = new HashSet<>(); + AtomicBoolean hasError = new AtomicBoolean(false); + + for (int i = 0; i < threadCount; i++) { + executor.submit(() -> { + for (int j = 0; j < iterationsPerThread; j++) { + String coded = StrUtil.codeId(testId); + String decoded = StrUtil.decodeId(coded); + + if (!testId.equals(decoded)) { + hasError.set(true); + break; + } + + synchronized (results) { + results.add(coded); + } + } + }); + } + + executor.shutdown(); + assertTrue(executor.awaitTermination(10, TimeUnit.SECONDS)); + assertFalse(hasError.get(), "codeId在多线程环境下解码失败"); + + // 验证生成的编码都是唯一的(由于使用SecureRandom) + assertEquals(threadCount * iterationsPerThread, results.size(), + "生成的编码应该都是唯一的"); + } + + @Test + @DisplayName("测试性能 - MD5计算") + void testMD5Performance() { + String testString = "Performance test string with some content to hash"; + long startTime = System.nanoTime(); + + for (int i = 0; i < 10000; i++) { + StrUtil.toMD5(testString); + } + + long endTime = System.nanoTime(); + long duration = endTime - startTime; + + // 确保性能在合理范围内(这里只是确保能完成,具体时间取决于硬件) + assertTrue(duration > 0, "MD5计算应该花费一些时间"); + + // 可以添加更具体的性能断言,但要考虑不同硬件的差异 + System.out.println("MD5 10000次计算耗时: " + duration / 1_000_000 + "ms"); + } + + @Test + @DisplayName("测试边界值") + void testBoundaryValues() { + // 测试空字符串 + String emptyResult = StrUtil.toMD5(""); + assertNotNull(emptyResult); + assertEquals(32, emptyResult.length()); + + // 测试单字符 + String singleCharResult = StrUtil.toMD5("a"); + assertNotNull(singleCharResult); + assertEquals(32, singleCharResult.length()); + + // 测试特殊字符 + String specialCharResult = StrUtil.toMD5("!@#$%^&*()"); + assertNotNull(specialCharResult); + assertEquals(32, specialCharResult.length()); + } +} \ No newline at end of file