diff --git a/infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalMd5UtilBoundaryTest.java b/infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalMd5UtilBoundaryTest.java new file mode 100644 index 00000000..c80ffcfb --- /dev/null +++ b/infra/common/src/test/java/cn/hippo4j/common/toolkit/IncrementalMd5UtilBoundaryTest.java @@ -0,0 +1,276 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hippo4j.common.toolkit; + +import cn.hippo4j.common.model.ThreadPoolParameterInfo; +import org.junit.Assert; +import org.junit.Test; + +/** + * Incremental MD5 Utility Boundary Test + * Tests edge cases and boundary conditions for version-aware MD5 calculation + */ +public class IncrementalMd5UtilBoundaryTest { + + /** + * Test: null config parameter + * Expected: Graceful handling without NPE + */ + @Test + public void testGetCoreMd5_NullConfig() { + System.out.println("========== Test 1: getCoreMd5 with null config =========="); + + try { + String md5 = IncrementalMd5Util.getCoreMd5(null); + System.out.println("Result MD5: " + md5); + System.out.println("Test passed: Handled null config gracefully"); + } catch (Exception e) { + System.out.println("Exception caught: " + e.getClass().getSimpleName()); + System.out.println("Test passed: NPE expected for null config"); + // NPE is acceptable for null input + } + } + + /** + * Test: config with all null fields + * Expected: Generates MD5 without error + */ + @Test + public void testGetCoreMd5_AllNullFields() { + System.out.println("\n========== Test 2: getCoreMd5 with all null fields =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + // All fields are null + + String md5 = IncrementalMd5Util.getCoreMd5(config); + + System.out.println("Config: all fields null"); + System.out.println("Generated MD5: " + md5); + + Assert.assertNotNull("MD5 should not be null", md5); + Assert.assertFalse("MD5 should not be empty", md5.isEmpty()); + System.out.println("Test passed: Generated MD5 for null fields"); + } + + /** + * Test: version number is 0 + * Expected: Treats as v1 (any version < 2 is v1) + */ + @Test + public void testGetVersionedMd5_VersionZero() { + System.out.println("\n========== Test 3: getVersionedMd5 with version = 0 =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + config.setCorePoolSize(10); + config.setMaximumPoolSize(20); + config.setExecuteTimeOut(5000L); + + String v0Md5 = IncrementalMd5Util.getVersionedMd5(config, 0); + String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1); + + System.out.println("Version 0 MD5: " + v0Md5); + System.out.println("Version 1 MD5: " + v1Md5); + System.out.println("Are they equal? " + v0Md5.equals(v1Md5)); + + Assert.assertEquals("Version 0 should behave like v1", v1Md5, v0Md5); + System.out.println("Test passed: Version 0 uses v1 behavior"); + } + + /** + * Test: version number is negative + * Expected: Treats as v1 (any version < 2 is v1) + */ + @Test + public void testGetVersionedMd5_NegativeVersion() { + System.out.println("\n========== Test 4: getVersionedMd5 with negative version =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + config.setCorePoolSize(10); + config.setMaximumPoolSize(20); + config.setExecuteTimeOut(5000L); + + String vNegativeMd5 = IncrementalMd5Util.getVersionedMd5(config, -1); + String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1); + + System.out.println("Version -1 MD5: " + vNegativeMd5); + System.out.println("Version 1 MD5: " + v1Md5); + System.out.println("Are they equal? " + vNegativeMd5.equals(v1Md5)); + + Assert.assertEquals("Negative version should behave like v1", v1Md5, vNegativeMd5); + System.out.println("Test passed: Negative version uses v1 behavior"); + } + + /** + * Test: very large version number + * Expected: Treats as v2+ (uses incremental MD5) + */ + @Test + public void testGetVersionedMd5_LargeVersion() { + System.out.println("\n========== Test 5: getVersionedMd5 with very large version =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + config.setCorePoolSize(10); + config.setMaximumPoolSize(20); + config.setExecuteTimeOut(5000L); + + String vLargeMd5 = IncrementalMd5Util.getVersionedMd5(config, 999999); + String v2Md5 = IncrementalMd5Util.getVersionedMd5(config, 2); + + System.out.println("Version 999999 MD5: " + vLargeMd5); + System.out.println("Version 2 MD5: " + v2Md5); + System.out.println("Are they equal? " + vLargeMd5.equals(v2Md5)); + + Assert.assertEquals("Large version should behave like v2", v2Md5, vLargeMd5); + System.out.println("Test passed: Large version uses v2 behavior"); + } + + /** + * Test: config with only extended parameters + * Expected: v2 generates MD5 for core params (even if empty) + */ + @Test + public void testGetVersionedMd5_OnlyExtendedParams() { + System.out.println("\n========== Test 6: Config with only extended parameters =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + // Only extended parameters + config.setExecuteTimeOut(5000L); + config.setIsAlarm(1); + config.setCapacityAlarm(80); + + String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1); + String v2Md5 = IncrementalMd5Util.getVersionedMd5(config, 2); + + System.out.println("Config: Only extended params (executeTimeOut, isAlarm, capacityAlarm)"); + System.out.println("v1 MD5: " + v1Md5); + System.out.println("v2 MD5: " + v2Md5); + System.out.println("Are they different? " + !v1Md5.equals(v2Md5)); + + Assert.assertNotEquals("v1 and v2 should differ when only extended params exist", v1Md5, v2Md5); + System.out.println("Test passed: v2 excludes extended params"); + } + + /** + * Test: isDifferent with same MD5 + * Expected: Returns false (no difference) + */ + @Test + public void testIsDifferent_SameMd5() { + System.out.println("\n========== Test 7: isDifferent with same MD5 =========="); + + ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo(); + oldConfig.setCorePoolSize(10); + oldConfig.setMaximumPoolSize(20); + + ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo(); + newConfig.setCorePoolSize(10); + newConfig.setMaximumPoolSize(20); + + boolean isDifferent = IncrementalMd5Util.isDifferent(oldConfig, newConfig, 2); + + System.out.println("Old and new configs are identical"); + System.out.println("isDifferent result: " + isDifferent); + + Assert.assertFalse("Should return false for identical configs", isDifferent); + System.out.println("Test passed: Identical configs correctly identified"); + } + + /** + * Test: isDifferent with different MD5 + * Expected: Returns true (difference detected) + */ + @Test + public void testIsDifferent_DifferentMd5() { + System.out.println("\n========== Test 8: isDifferent with different MD5 =========="); + + ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo(); + oldConfig.setCorePoolSize(10); + oldConfig.setMaximumPoolSize(20); + + ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo(); + newConfig.setCorePoolSize(15); // Different! + newConfig.setMaximumPoolSize(20); + + boolean isDifferent = IncrementalMd5Util.isDifferent(oldConfig, newConfig, 2); + + System.out.println("Old corePoolSize: 10, New corePoolSize: 15"); + System.out.println("isDifferent result: " + isDifferent); + + Assert.assertTrue("Should return true for different configs", isDifferent); + System.out.println("Test passed: Different configs correctly identified"); + } + + /** + * Test: Empty string vs null for string fields + * Expected: Both generate valid MD5 + */ + @Test + public void testGetCoreMd5_EmptyVsNullString() { + System.out.println("\n========== Test 9: Empty string vs null for string fields =========="); + + ThreadPoolParameterInfo configWithNull = new ThreadPoolParameterInfo(); + configWithNull.setTenantId(null); + configWithNull.setItemId(null); + configWithNull.setTpId(null); + + ThreadPoolParameterInfo configWithEmpty = new ThreadPoolParameterInfo(); + configWithEmpty.setTenantId(""); + configWithEmpty.setItemId(""); + configWithEmpty.setTpId(""); + + String nullMd5 = IncrementalMd5Util.getCoreMd5(configWithNull); + String emptyMd5 = IncrementalMd5Util.getCoreMd5(configWithEmpty); + + System.out.println("Null fields MD5: " + nullMd5); + System.out.println("Empty fields MD5: " + emptyMd5); + System.out.println("Are they different? " + !nullMd5.equals(emptyMd5)); + + Assert.assertNotNull("Null fields MD5 should not be null", nullMd5); + Assert.assertNotNull("Empty fields MD5 should not be null", emptyMd5); + System.out.println("Test passed: Both null and empty strings handled"); + } + + /** + * Test: Consistency - multiple calls should return same MD5 + * Expected: Same MD5 for same config + */ + @Test + public void testGetCoreMd5_Consistency() { + System.out.println("\n========== Test 10: MD5 calculation consistency =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + config.setTenantId("test"); + config.setItemId("test"); + config.setTpId("test"); + config.setCorePoolSize(10); + config.setMaximumPoolSize(20); + + String md51 = IncrementalMd5Util.getCoreMd5(config); + String md52 = IncrementalMd5Util.getCoreMd5(config); + String md53 = IncrementalMd5Util.getCoreMd5(config); + + System.out.println("MD5 call 1: " + md51); + System.out.println("MD5 call 2: " + md52); + System.out.println("MD5 call 3: " + md53); + System.out.println("All equal? " + (md51.equals(md52) && md52.equals(md53))); + + Assert.assertEquals("Multiple calls should return same MD5", md51, md52); + Assert.assertEquals("Multiple calls should return same MD5", md52, md53); + System.out.println("Test passed: MD5 calculation is consistent"); + } +} diff --git a/threadpool/server/config/src/main/java/cn/hippo4j/config/toolkit/Md5ConfigUtil.java b/threadpool/server/config/src/main/java/cn/hippo4j/config/toolkit/Md5ConfigUtil.java index 67e58efa..f7c5bb62 100644 --- a/threadpool/server/config/src/main/java/cn/hippo4j/config/toolkit/Md5ConfigUtil.java +++ b/threadpool/server/config/src/main/java/cn/hippo4j/config/toolkit/Md5ConfigUtil.java @@ -75,7 +75,7 @@ public class Md5ConfigUtil { int clientVersion = getClientVersion(request); clientMd5Map.forEach((key, val) -> { String clientIdentify = RequestUtil.getClientIdentify(request); - boolean isUpdateData = ConfigCacheService.isUpdateData(key, val, clientIdentify,clientVersion); + boolean isUpdateData = ConfigCacheService.isUpdateData(key, val, clientIdentify, clientVersion); if (!isUpdateData) { changedGroupKeys.add(key); } diff --git a/threadpool/server/config/src/test/java/cn/hippo4j/config/service/ConfigCacheServiceVersionTest.java b/threadpool/server/config/src/test/java/cn/hippo4j/config/service/ConfigCacheServiceVersionTest.java new file mode 100644 index 00000000..9db3dd5f --- /dev/null +++ b/threadpool/server/config/src/test/java/cn/hippo4j/config/service/ConfigCacheServiceVersionTest.java @@ -0,0 +1,180 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hippo4j.config.service; + +import cn.hippo4j.common.model.ThreadPoolParameterInfo; +import cn.hippo4j.common.toolkit.IncrementalMd5Util; +import org.junit.Assert; +import org.junit.Test; + +/** + * ConfigCacheService Version-Aware Test + * Tests the version-aware MD5 comparison logic for cross-version compatibility + * + * Note: Tests focus on the MD5 calculation logic used by ConfigCacheService + * Integration tests for ConfigCacheService.isUpdateData() are covered separately + * due to dependencies on configuration cache infrastructure + */ +public class ConfigCacheServiceVersionTest { + + /** + * Test: Version-aware MD5 comparison logic + * Verifies that v1 and v2 produce different MD5 for same config with extended params + */ + @Test + public void testVersionedMd5_Difference() { + System.out.println("========== Test 1: v1 vs v2 MD5 difference =========="); + + ThreadPoolParameterInfo config = new ThreadPoolParameterInfo(); + config.setTenantId("test-tenant"); + config.setItemId("test-item"); + config.setTpId("test-pool"); + config.setCorePoolSize(10); + config.setMaximumPoolSize(20); + config.setQueueType(2); + config.setCapacity(1024); + config.setKeepAliveTime(60L); + config.setRejectedType(1); + config.setAllowCoreThreadTimeOut(0); + config.setExecuteTimeOut(5000L); // Extended parameter + config.setIsAlarm(1); // Extended parameter + + String v1Md5 = IncrementalMd5Util.getVersionedMd5(config, 1); + String v2Md5 = IncrementalMd5Util.getVersionedMd5(config, 2); + + System.out.println("Config includes extended parameters: executeTimeOut, isAlarm"); + System.out.println("v1 MD5 (full): " + v1Md5); + System.out.println("v2 MD5 (incremental): " + v2Md5); + System.out.println("Are they different? " + !v1Md5.equals(v2Md5)); + + Assert.assertNotEquals("v1 and v2 should produce different MD5", v1Md5, v2Md5); + System.out.println("Test passed: v1 and v2 use different MD5 strategies"); + } + + /** + * Test: v2 client should not refresh when only extended params change + * This is the core extensibility improvement + */ + @Test + public void testV2Client_ExtendedParamChange_NoRefresh() { + System.out.println("\n========== Test 2: v2 client - extended param change =========="); + + // Old config + ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo(); + oldConfig.setCorePoolSize(10); + oldConfig.setMaximumPoolSize(20); + oldConfig.setQueueType(2); + oldConfig.setCapacity(1024); + oldConfig.setKeepAliveTime(60L); + oldConfig.setRejectedType(1); + oldConfig.setAllowCoreThreadTimeOut(0); + oldConfig.setExecuteTimeOut(3000L); // Extended + + // New config (only extended param changed) + ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo(); + newConfig.setCorePoolSize(10); + newConfig.setMaximumPoolSize(20); + newConfig.setQueueType(2); + newConfig.setCapacity(1024); + newConfig.setKeepAliveTime(60L); + newConfig.setRejectedType(1); + newConfig.setAllowCoreThreadTimeOut(0); + newConfig.setExecuteTimeOut(5000L); // Extended param changed! + + String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2); + String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2); + + System.out.println("Extended param change: executeTimeOut 3000 -> 5000"); + System.out.println("Old v2 MD5: " + oldV2Md5); + System.out.println("New v2 MD5: " + newV2Md5); + System.out.println("Are they same? " + oldV2Md5.equals(newV2Md5)); + + Assert.assertEquals("v2 MD5 should remain same (no core param change)", oldV2Md5, newV2Md5); + System.out.println("Test passed: v2 client won't refresh for extended param change"); + } + + /** + * Test: v1 client SHOULD refresh when extended params change + * This verifies backward compatibility + */ + @Test + public void testV1Client_ExtendedParamChange_ShouldRefresh() { + System.out.println("\n========== Test 3: v1 client - extended param change =========="); + + // Old config + ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo(); + oldConfig.setCorePoolSize(10); + oldConfig.setMaximumPoolSize(20); + oldConfig.setQueueType(2); + oldConfig.setCapacity(1024); + oldConfig.setExecuteTimeOut(3000L); + + // New config (only extended param changed) + ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo(); + newConfig.setCorePoolSize(10); + newConfig.setMaximumPoolSize(20); + newConfig.setQueueType(2); + newConfig.setCapacity(1024); + newConfig.setExecuteTimeOut(5000L); // Extended param changed! + + String oldV1Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 1); + String newV1Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 1); + + System.out.println("Extended param change: executeTimeOut 3000 -> 5000"); + System.out.println("Old v1 MD5: " + oldV1Md5); + System.out.println("New v1 MD5: " + newV1Md5); + System.out.println("Are they different? " + !oldV1Md5.equals(newV1Md5)); + + Assert.assertNotEquals("v1 MD5 should differ (uses full comparison)", oldV1Md5, newV1Md5); + System.out.println("Test passed: v1 client will refresh for extended param change"); + } + + /** + * Test: Both v1 and v2 should refresh when core params change + * This ensures core functionality still works for both versions + */ + @Test + public void testBothVersions_CoreParamChange_ShouldRefresh() { + System.out.println("\n========== Test 4: Both versions - core param change =========="); + + // Old config + ThreadPoolParameterInfo oldConfig = new ThreadPoolParameterInfo(); + oldConfig.setCorePoolSize(10); + oldConfig.setMaximumPoolSize(20); + oldConfig.setQueueType(2); + + // New config (core param changed) + ThreadPoolParameterInfo newConfig = new ThreadPoolParameterInfo(); + newConfig.setCorePoolSize(15); // Core param changed! + newConfig.setMaximumPoolSize(20); + newConfig.setQueueType(2); + + String oldV1Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 1); + String newV1Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 1); + String oldV2Md5 = IncrementalMd5Util.getVersionedMd5(oldConfig, 2); + String newV2Md5 = IncrementalMd5Util.getVersionedMd5(newConfig, 2); + + System.out.println("Core param change: corePoolSize 10 -> 15"); + System.out.println("v1: " + oldV1Md5 + " -> " + newV1Md5 + " (different? " + !oldV1Md5.equals(newV1Md5) + ")"); + System.out.println("v2: " + oldV2Md5 + " -> " + newV2Md5 + " (different? " + !oldV2Md5.equals(newV2Md5) + ")"); + + Assert.assertNotEquals("v1 should detect core param change", oldV1Md5, newV1Md5); + Assert.assertNotEquals("v2 should detect core param change", oldV2Md5, newV2Md5); + System.out.println("Test passed: Both versions detect core param changes"); + } +} diff --git a/threadpool/server/config/src/test/java/cn/hippo4j/config/toolkit/Md5ConfigUtilVersionTest.java b/threadpool/server/config/src/test/java/cn/hippo4j/config/toolkit/Md5ConfigUtilVersionTest.java new file mode 100644 index 00000000..6e9b5403 --- /dev/null +++ b/threadpool/server/config/src/test/java/cn/hippo4j/config/toolkit/Md5ConfigUtilVersionTest.java @@ -0,0 +1,203 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package cn.hippo4j.config.toolkit; + +import org.junit.Assert; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +import javax.servlet.http.HttpServletRequest; +import java.lang.reflect.Method; + +import static org.mockito.Mockito.when; + +/** + * Md5ConfigUtil Version Detection Test + * Tests the client protocol version detection logic for cross-version compatibility + */ +@RunWith(MockitoJUnitRunner.class) +public class Md5ConfigUtilVersionTest { + + @Mock + private HttpServletRequest request; + + private static final String PROTOCOL_VERSION_HEADER = "X-Hippo4j-Protocol-Version"; + + /** + * Test: Client sends v2 protocol version header + * Expected: Server correctly identifies client as v2 + */ + @Test + public void testGetClientVersion_WithV2Header() throws Exception { + System.out.println("========== Test 1: Client sends v2 version header =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("2"); + + int version = invokeGetClientVersion(request); + + System.out.println("Request header: X-Hippo4j-Protocol-Version = 2"); + System.out.println("Detected client version: " + version); + + Assert.assertEquals("Should detect v2 client", 2, version); + System.out.println("Test passed: Server correctly identifies v2 client"); + } + + /** + * Test: Old client doesn't send version header + * Expected: Server defaults to v1 for backward compatibility + */ + @Test + public void testGetClientVersion_WithoutHeader() throws Exception { + System.out.println("\n========== Test 2: Old client without version header =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn(null); + + int version = invokeGetClientVersion(request); + + System.out.println("Request header: X-Hippo4j-Protocol-Version = null"); + System.out.println("Detected client version: " + version); + + Assert.assertEquals("Should default to v1", 1, version); + System.out.println("Test passed: Server defaults to v1 for backward compatibility"); + } + + /** + * Test: Version header is empty string + * Expected: Server defaults to v1 + */ + @Test + public void testGetClientVersion_WithEmptyHeader() throws Exception { + System.out.println("\n========== Test 3: Version header is empty string =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn(""); + + int version = invokeGetClientVersion(request); + + System.out.println("Request header: X-Hippo4j-Protocol-Version = \"\""); + System.out.println("Detected client version: " + version); + + Assert.assertEquals("Should default to v1 for empty header", 1, version); + System.out.println("Test passed: Empty header defaults to v1"); + } + + /** + * Test: Version header has invalid format (non-numeric) + * Expected: Server gracefully handles error and defaults to v1 + */ + @Test + public void testGetClientVersion_WithInvalidFormat() throws Exception { + System.out.println("\n========== Test 4: Version header has invalid format =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("invalid"); + + int version = invokeGetClientVersion(request); + + System.out.println("Request header: X-Hippo4j-Protocol-Version = \"invalid\""); + System.out.println("Detected client version: " + version); + + Assert.assertEquals("Should default to v1 for invalid format", 1, version); + System.out.println("Test passed: Invalid format gracefully handled"); + } + + /** + * Test: Version header is v1 explicitly + * Expected: Server correctly identifies as v1 + */ + @Test + public void testGetClientVersion_WithV1Header() throws Exception { + System.out.println("\n========== Test 5: Client explicitly sends v1 =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("1"); + + int version = invokeGetClientVersion(request); + + System.out.println("Request header: X-Hippo4j-Protocol-Version = 1"); + System.out.println("Detected client version: " + version); + + Assert.assertEquals("Should detect v1 client", 1, version); + System.out.println("Test passed: Server correctly identifies v1 client"); + } + + /** + * Test: Version header is future version (v3) + * Expected: Server correctly parses and returns the version + */ + @Test + public void testGetClientVersion_WithFutureVersion() throws Exception { + System.out.println("\n========== Test 6: Client sends future version (v3) =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("3"); + + int version = invokeGetClientVersion(request); + + System.out.println("Request header: X-Hippo4j-Protocol-Version = 3"); + System.out.println("Detected client version: " + version); + + Assert.assertEquals("Should parse future version", 3, version); + System.out.println("Test passed: Future version handled correctly"); + } + + /** + * Test: compareMd5 method can be called with v1 client + * Note: This is a lightweight test focusing on version detection + * Full integration tests are covered separately + */ + @Test + public void testCompareMd5_VersionDetection_V1Client() throws Exception { + System.out.println("\n========== Test 7: Version detection in compareMd5 (v1) =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn(null); + + // Verify version detection works + int version = invokeGetClientVersion(request); + + System.out.println("Client version detected: v" + version); + Assert.assertEquals("Should detect v1 client", 1, version); + System.out.println("Test passed: compareMd5 will use v1 protocol for this client"); + } + + /** + * Test: compareMd5 method can be called with v2 client + * Note: This is a lightweight test focusing on version detection + * Full integration tests are covered separately + */ + @Test + public void testCompareMd5_VersionDetection_V2Client() throws Exception { + System.out.println("\n========== Test 8: Version detection in compareMd5 (v2) =========="); + + when(request.getHeader(PROTOCOL_VERSION_HEADER)).thenReturn("2"); + + // Verify version detection works + int version = invokeGetClientVersion(request); + + System.out.println("Client version detected: v" + version); + Assert.assertEquals("Should detect v2 client", 2, version); + System.out.println("Test passed: compareMd5 will use v2 protocol for this client"); + } + + /** + * Helper method to invoke private getClientVersion method via reflection + */ + private int invokeGetClientVersion(HttpServletRequest request) throws Exception { + Method method = Md5ConfigUtil.class.getDeclaredMethod("getClientVersion", HttpServletRequest.class); + method.setAccessible(true); + return (int) method.invoke(null, request); + } +}