diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/aspect/controller/SuperAdminPasswordSafeAdviceTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/aspect/controller/SuperAdminPasswordSafeAdviceTest.java new file mode 100644 index 000000000000..22078bc05757 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/aspect/controller/SuperAdminPasswordSafeAdviceTest.java @@ -0,0 +1,174 @@ +/* + * 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 org.apache.shenyu.admin.aspect.controller; + +import com.google.common.base.Stopwatch; +import org.apache.shenyu.admin.config.properties.DashboardProperties; +import org.apache.shenyu.admin.model.custom.UserInfo; +import org.apache.shenyu.admin.service.DashboardUserService; +import org.apache.shenyu.admin.utils.SessionUtil; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.core.annotation.AnnotatedElementUtils; + +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.List; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test cases for {@link SuperAdminPasswordSafeAdvice}. + */ +@ExtendWith(MockitoExtension.class) +public class SuperAdminPasswordSafeAdviceTest { + + @Mock + private DashboardProperties properties; + + @Mock + private DashboardUserService userService; + + @InjectMocks + private SuperAdminPasswordSafeAdvice advice; + + private Object bean; + + private Method method; + + private Stopwatch stopwatch; + + @BeforeEach + void setUp() throws Exception { + bean = new Object(); + method = Object.class.getMethod("toString"); + stopwatch = Stopwatch.createUnstarted(); + } + + @Test + void testDoPreProcessWhenEnableOnlySuperAdminPermissionIsFalseShouldSkip() { + when(properties.getEnableOnlySuperAdminPermission()).thenReturn(false); + + advice.doPreProcess(bean, method, stopwatch); + + verify(properties, never()).getEnableSuperAdminPasswordSafe(); + verify(userService, never()).checkUserPassword(anyString()); + } + + @Test + void testDoPreProcessWhenEnableSuperAdminPasswordSafeIsFalseShouldSkip() { + when(properties.getEnableOnlySuperAdminPermission()).thenReturn(true); + when(properties.getEnableSuperAdminPasswordSafe()).thenReturn(false); + + advice.doPreProcess(bean, method, stopwatch); + + verify(userService, never()).checkUserPassword(anyString()); + } + + @Test + void testDoPreProcessWhenNotAdminShouldSkip() { + when(properties.getEnableOnlySuperAdminPermission()).thenReturn(true); + when(properties.getEnableSuperAdminPasswordSafe()).thenReturn(true); + + try (MockedStatic sessionUtilMock = mockStatic(SessionUtil.class)) { + sessionUtilMock.when(SessionUtil::isAdmin).thenReturn(false); + + advice.doPreProcess(bean, method, stopwatch); + + verify(userService, never()).checkUserPassword(anyString()); + } + } + + @Test + void testDoPreProcessWhenNoRequiresPermissionsShouldSkip() { + when(properties.getEnableOnlySuperAdminPermission()).thenReturn(true); + when(properties.getEnableSuperAdminPasswordSafe()).thenReturn(true); + + try (MockedStatic sessionUtilMock = mockStatic(SessionUtil.class)) { + sessionUtilMock.when(SessionUtil::isAdmin).thenReturn(true); + + advice.doPreProcess(bean, method, stopwatch); + + verify(userService, never()).checkUserPassword(anyString()); + } + } + + @Test + void testDoPreProcessWhenPermissionNotInListShouldSkip() throws Exception { + when(properties.getEnableOnlySuperAdminPermission()).thenReturn(true); + when(properties.getEnableSuperAdminPasswordSafe()).thenReturn(true); + + Method methodWithAnnotation = TestController.class.getMethod("testMethod"); + UserInfo user = mock(UserInfo.class); + + try (MockedStatic sessionUtilMock = mockStatic(SessionUtil.class); + MockedStatic annotatedElementUtilsMock = mockStatic(AnnotatedElementUtils.class)) { + sessionUtilMock.when(SessionUtil::isAdmin).thenReturn(true); + sessionUtilMock.when(SessionUtil::visitor).thenReturn(user); + annotatedElementUtilsMock.when(() -> AnnotatedElementUtils.findMergedAnnotation(methodWithAnnotation, org.apache.shiro.authz.annotation.RequiresPermissions.class)) + .thenReturn(mock(org.apache.shiro.authz.annotation.RequiresPermissions.class)); + + advice.doPreProcess(bean, methodWithAnnotation, stopwatch); + + verify(userService, never()).checkUserPassword(anyString()); + } + } + + @Test + void testDoPreProcessWhenPermissionInListShouldCheckPassword() throws Exception { + when(properties.getEnableOnlySuperAdminPermission()).thenReturn(true); + when(properties.getEnableSuperAdminPasswordSafe()).thenReturn(true); + List permissions = Arrays.asList("admin:permission", "other:permission"); + when(properties.getOnlySuperAdminPermission()).thenReturn(permissions); + + Method methodWithAnnotation = TestController.class.getMethod("testMethod"); + org.apache.shiro.authz.annotation.RequiresPermissions requiresPermissions = mock(org.apache.shiro.authz.annotation.RequiresPermissions.class); + when(requiresPermissions.value()).thenReturn(new String[]{"admin:permission"}); + UserInfo user = mock(UserInfo.class); + when(user.getUserId()).thenReturn("userId"); + + try (MockedStatic sessionUtilMock = mockStatic(SessionUtil.class); + MockedStatic annotatedElementUtilsMock = mockStatic(AnnotatedElementUtils.class)) { + sessionUtilMock.when(SessionUtil::isAdmin).thenReturn(true); + sessionUtilMock.when(SessionUtil::visitor).thenReturn(user); + annotatedElementUtilsMock.when(() -> AnnotatedElementUtils.findMergedAnnotation(methodWithAnnotation, org.apache.shiro.authz.annotation.RequiresPermissions.class)) + .thenReturn(requiresPermissions); + + advice.doPreProcess(bean, methodWithAnnotation, stopwatch); + + verify(userService).checkUserPassword("userId"); + } + } + + private static class TestController { + @org.apache.shiro.authz.annotation.RequiresPermissions("admin:permission") + public void testMethod() { + throw new UnsupportedOperationException("testMethod() used for unit test."); + } + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/DashboardPropertiesTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/DashboardPropertiesTest.java index 63359e88cf08..2bac2c04e08a 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/DashboardPropertiesTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/config/properties/DashboardPropertiesTest.java @@ -17,13 +17,22 @@ package org.apache.shenyu.admin.config.properties; +import org.apache.shenyu.admin.AbstractConfigurationTest; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.context.annotation.Configuration; + +import java.util.ArrayList; +import java.util.List; /** * Test cases for {@link DashboardProperties}. */ -public class DashboardPropertiesTest { +@ExtendWith(MockitoExtension.class) +public class DashboardPropertiesTest extends AbstractConfigurationTest { @Test public void dashboardPropertiesTest() { @@ -31,8 +40,78 @@ public void dashboardPropertiesTest() { dashboardProperties.setOnlyCleanDays(0); dashboardProperties.setEnablePrintApiLog(true); dashboardProperties.setRecordLogLimit(0); - Assertions.assertEquals(dashboardProperties.getOnlyCleanDays(), 0); - Assertions.assertEquals(dashboardProperties.getEnablePrintApiLog(), true); - Assertions.assertEquals(dashboardProperties.getRecordLogLimit(), 0); + dashboardProperties.setEnableOnlySuperAdminPermission(false); + dashboardProperties.setEnableSuperAdminPasswordSafe(false); + dashboardProperties.setSuperAdminPasswordValidDuration(0L); + + Assertions.assertEquals(0, dashboardProperties.getOnlyCleanDays()); + Assertions.assertEquals(true, dashboardProperties.getEnablePrintApiLog()); + Assertions.assertEquals(0, dashboardProperties.getRecordLogLimit()); + Assertions.assertEquals(false, dashboardProperties.getEnableOnlySuperAdminPermission()); + Assertions.assertEquals(false, dashboardProperties.getEnableSuperAdminPasswordSafe()); + Assertions.assertEquals(0, dashboardProperties.getSuperAdminPasswordValidDuration()); + } + + @Test + public void testSpecified() { + load(DashboardPropertiesTest.DashboardPropertiesConfiguration.class, + "shenyu.dashboard.core.recordLogLimit=10", + "shenyu.dashboard.core.onlyCleanDays=1", + "shenyu.dashboard.core.enablePrintApiLog=true", + "shenyu.dashboard.core.enableOnlySuperAdminPermission=false", + "shenyu.dashboard.core.enableSuperAdminPasswordSafe=false", + "shenyu.dashboard.core.superAdminPasswordValidDuration=0"); + + List onlySuperAdminPermission = new ArrayList<>(); + onlySuperAdminPermission.add("1"); + DashboardProperties dashboardProperties = getContext().getBean(DashboardProperties.class); + dashboardProperties.setOnlySuperAdminPermission(onlySuperAdminPermission); + + Assertions.assertEquals(10, dashboardProperties.getRecordLogLimit()); + Assertions.assertEquals(1, dashboardProperties.getOnlyCleanDays()); + Assertions.assertTrue(dashboardProperties.getEnablePrintApiLog()); + Assertions.assertFalse(dashboardProperties.getEnableOnlySuperAdminPermission()); + Assertions.assertFalse(dashboardProperties.getEnableSuperAdminPasswordSafe()); + Assertions.assertEquals(0L, dashboardProperties.getSuperAdminPasswordValidDuration()); + Assertions.assertEquals(1, dashboardProperties.getOnlySuperAdminPermission().size()); + Assertions.assertEquals("1", dashboardProperties.getOnlySuperAdminPermission().get(0)); + } + + @Test + public void testOnlySuperAdminPermission() { + final DashboardProperties dashboardProperties = new DashboardProperties(); + dashboardProperties.afterPropertiesSet(); + Assertions.assertEquals(12, dashboardProperties.getOnlySuperAdminPermission().size()); + // Check user permissions + Assertions.assertEquals("system:manager:add", dashboardProperties.getOnlySuperAdminPermission().get(0)); + Assertions.assertEquals("system:manager:edit", dashboardProperties.getOnlySuperAdminPermission().get(1)); + Assertions.assertEquals("system:manager:delete", dashboardProperties.getOnlySuperAdminPermission().get(2)); + + // Check role permissions + Assertions.assertEquals("system:role:edit", dashboardProperties.getOnlySuperAdminPermission().get(3)); + Assertions.assertEquals("system:role:add", dashboardProperties.getOnlySuperAdminPermission().get(4)); + Assertions.assertEquals("system:role:delete", dashboardProperties.getOnlySuperAdminPermission().get(5)); + + // Check resource permissions + Assertions.assertEquals("system:resource:addButton", dashboardProperties.getOnlySuperAdminPermission().get(6)); + Assertions.assertEquals("system:resource:addMenu", dashboardProperties.getOnlySuperAdminPermission().get(7)); + Assertions.assertEquals("system:resource:editButton", dashboardProperties.getOnlySuperAdminPermission().get(8)); + Assertions.assertEquals("system:resource:editMenu", dashboardProperties.getOnlySuperAdminPermission().get(9)); + Assertions.assertEquals("system:resource:deleteMenu", dashboardProperties.getOnlySuperAdminPermission().get(10)); + Assertions.assertEquals("system:resource:deleteButton", dashboardProperties.getOnlySuperAdminPermission().get(11)); + + List onlySuperAdminPermission = new ArrayList<>(); + onlySuperAdminPermission.add("1"); + dashboardProperties.setOnlySuperAdminPermission(onlySuperAdminPermission); + Assertions.assertEquals(1, dashboardProperties.getOnlySuperAdminPermission().size()); + + dashboardProperties.setEnableOnlySuperAdminPermission(false); + dashboardProperties.afterPropertiesSet(); + Assertions.assertEquals(0, dashboardProperties.getOnlySuperAdminPermission().size()); + } + + @Configuration + @EnableConfigurationProperties(DashboardProperties.class) + static class DashboardPropertiesConfiguration { } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/RegisterClientServerDisruptorPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/RegisterClientServerDisruptorPublisherTest.java new file mode 100644 index 000000000000..d95b3bac6d0c --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/RegisterClientServerDisruptorPublisherTest.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 org.apache.shenyu.admin.disruptor; + +import org.apache.shenyu.admin.service.DiscoveryService; +import org.apache.shenyu.admin.service.register.ShenyuClientRegisterService; +import org.apache.shenyu.disruptor.DisruptorProviderManage; +import org.apache.shenyu.disruptor.provider.DisruptorProvider; +import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO; +import org.apache.shenyu.register.common.dto.URIRegisterDTO; +import org.apache.shenyu.register.common.type.DataTypeParent; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Field; +import java.util.ArrayList; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; + +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link RegisterClientServerDisruptorPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class RegisterClientServerDisruptorPublisherTest { + + @Mock + private ShenyuClientRegisterService shenyuClientRegisterService; + + @Mock + private DiscoveryService discoveryService; + + @Mock + private DisruptorProviderManage> mockProviderManage; + + @Mock + private DisruptorProvider> mockProvider; + + private RegisterClientServerDisruptorPublisher publisher; + + private Map serviceMap; + + @BeforeEach + void setUp() { + publisher = RegisterClientServerDisruptorPublisher.getInstance(); + serviceMap = new HashMap<>(); + serviceMap.put("http", shenyuClientRegisterService); + } + + @AfterEach + void tearDown() throws Exception { + Field providerManageField = RegisterClientServerDisruptorPublisher.class.getDeclaredField("providerManage"); + providerManageField.setAccessible(true); + Object providerManage = providerManageField.get(publisher); + if (Objects.nonNull(providerManage)) { + try { + publisher.close(); + } catch (Exception ignored) { + // Ignore exceptions during cleanup to avoid test failures + } + } + providerManageField.set(publisher, null); + } + + @Test + void testGetInstance() { + RegisterClientServerDisruptorPublisher instance1 = RegisterClientServerDisruptorPublisher.getInstance(); + RegisterClientServerDisruptorPublisher instance2 = RegisterClientServerDisruptorPublisher.getInstance(); + + assertNotNull(instance1); + assertSame(instance1, instance2); + } + + @Test + void testStart() { + publisher.start(serviceMap, discoveryService); + + assertNotNull(getProviderManage()); + } + + @Test + void testPublishSingleData() throws Exception { + publisher.start(serviceMap, discoveryService); + setMockProviderManage(); + + URIRegisterDTO uriDTO = URIRegisterDTO.builder() + .contextPath("/test") + .rpcType("http") + .namespaceId("default") + .build(); + + publisher.publish(uriDTO); + + verify(mockProvider, times(1)).onData(any()); + } + + @Test + void testPublishCollectionData() throws Exception { + publisher.start(serviceMap, discoveryService); + setMockProviderManage(); + + List dataList = new ArrayList<>(); + dataList.add(URIRegisterDTO.builder() + .contextPath("/test1") + .rpcType("http") + .namespaceId("default") + .build()); + dataList.add(URIRegisterDTO.builder() + .contextPath("/test2") + .rpcType("http") + .namespaceId("default") + .build()); + + publisher.publish(dataList); + + verify(mockProvider, times(1)).onData(any()); + } + + @Test + void testPublishMetaData() throws Exception { + publisher.start(serviceMap, discoveryService); + setMockProviderManage(); + + MetaDataRegisterDTO metaDTO = MetaDataRegisterDTO.builder() + .appName("testApp") + .path("/test/path") + .ruleName("testRule") + .rpcType("http") + .namespaceId("default") + .build(); + + publisher.publish(metaDTO); + + verify(mockProvider, times(1)).onData(any()); + } + + @Test + void testClose() throws Exception { + publisher.start(serviceMap, discoveryService); + setMockProviderManage(); + + publisher.close(); + + verify(mockProvider, times(1)).shutdown(); + } + + @Test + void testPublishEmptyCollection() throws Exception { + publisher.start(serviceMap, discoveryService); + setMockProviderManage(); + + List emptyList = new ArrayList<>(); + publisher.publish(emptyList); + + verify(mockProvider, times(1)).onData(any()); + } + + private DisruptorProviderManage> getProviderManage() { + try { + Field field = RegisterClientServerDisruptorPublisher.class.getDeclaredField("providerManage"); + field.setAccessible(true); + return (DisruptorProviderManage>) field.get(publisher); + } catch (Exception e) { + return null; + } + } + + private void setMockProviderManage() throws Exception { + when(mockProviderManage.getProvider()).thenReturn(mockProvider); + + Field field = RegisterClientServerDisruptorPublisher.class.getDeclaredField("providerManage"); + field.setAccessible(true); + field.set(publisher, mockProviderManage); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/executor/RegisterServerConsumerExecutorTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/executor/RegisterServerConsumerExecutorTest.java new file mode 100644 index 000000000000..dae8ca2566c4 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/executor/RegisterServerConsumerExecutorTest.java @@ -0,0 +1,338 @@ +/* + * 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 org.apache.shenyu.admin.disruptor.executor; + +import org.apache.shenyu.disruptor.consumer.QueueConsumerExecutor; +import org.apache.shenyu.register.common.dto.MetaDataRegisterDTO; +import org.apache.shenyu.register.common.dto.URIRegisterDTO; +import org.apache.shenyu.register.common.subsriber.ExecutorSubscriber; +import org.apache.shenyu.register.common.subsriber.ExecutorTypeSubscriber; +import org.apache.shenyu.register.common.type.DataType; +import org.apache.shenyu.register.common.type.DataTypeParent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test cases for {@link RegisterServerConsumerExecutor}. + */ +@ExtendWith(MockitoExtension.class) +class RegisterServerConsumerExecutorTest { + + @Mock + private ExecutorTypeSubscriber mockSubscriber; + + @Mock + private ExecutorSubscriber mockExecutorSubscriber; + + private RegisterServerConsumerExecutor.RegisterServerExecutorFactory factory; + + @BeforeEach + void setUp() { + factory = new RegisterServerConsumerExecutor.RegisterServerExecutorFactory(); + } + + @Test + void testFactoryCreate() { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + + QueueConsumerExecutor> executor = factory.create(); + + assertNotNull(executor); + assertTrue(executor instanceof RegisterServerConsumerExecutor); + } + + @Test + void testFactoryFixName() { + assertEquals("shenyu_register_server", factory.fixName()); + } + + @Test + void testFactoryAddSubscribers() { + RegisterServerConsumerExecutor.RegisterServerExecutorFactory result = factory.addSubscribers(mockSubscriber); + + assertEquals(factory, result); + assertEquals(1, factory.getSubscribers().size()); + assertTrue(factory.getSubscribers().contains(mockSubscriber)); + } + + @Test + void testFactoryGetSubscribers() { + factory.addSubscribers(mockSubscriber); + + assertEquals(1, factory.getSubscribers().size()); + } + + @Test + void testRunWithEmptyData() { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + executor.setData(Collections.emptyList()); + executor.run(); + + verify(mockSubscriber, never()).executor(any()); + } + + @Test + void testRunWithValidURIData() { + when(mockSubscriber.getType()).thenReturn(DataType.URI); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + URIRegisterDTO uriDTO = URIRegisterDTO.builder() + .contextPath("/test") + .rpcType("http") + .namespaceId("default") + .build(); + + List data = new ArrayList<>(); + data.add(uriDTO); + + executor.setData(data); + executor.run(); + + verify(mockSubscriber, times(1)).executor(any()); + } + + @Test + void testRunWithValidMetaData() { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + MetaDataRegisterDTO metaDTO = MetaDataRegisterDTO.builder() + .appName("testApp") + .path("/test/path") + .ruleName("testRule") + .rpcType("http") + .namespaceId("default") + .build(); + + List data = new ArrayList<>(); + data.add(metaDTO); + + executor.setData(data); + executor.run(); + + verify(mockSubscriber, times(1)).executor(any()); + } + + @Test + void testRunWithInvalidURIData() { + when(mockSubscriber.getType()).thenReturn(DataType.URI); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + URIRegisterDTO uriDTO = URIRegisterDTO.builder() + .contextPath("") + .rpcType("http") + .namespaceId("default") + .build(); + + List data = new ArrayList<>(); + data.add(uriDTO); + + executor.setData(data); + executor.run(); + + verify(mockSubscriber, never()).executor(any()); + } + + @Test + void testRunWithInvalidMetaData() { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + MetaDataRegisterDTO metaDTO = MetaDataRegisterDTO.builder() + .appName("") + .path("/test/path") + .ruleName("testRule") + .rpcType("http") + .namespaceId("default") + .build(); + + List data = new ArrayList<>(); + data.add(metaDTO); + + executor.setData(data); + executor.run(); + + verify(mockSubscriber, never()).executor(any()); + } + + @Test + void testIsValidDataWithValidURI() throws Exception { + when(mockSubscriber.getType()).thenReturn(DataType.URI); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + URIRegisterDTO uriDTO = URIRegisterDTO.builder() + .contextPath("/test") + .rpcType("http") + .namespaceId("default") + .build(); + + Method method = RegisterServerConsumerExecutor.class.getDeclaredMethod("isValidData", Object.class); + method.setAccessible(true); + boolean result = (boolean) method.invoke(executor, uriDTO); + + assertTrue(result); + } + + @Test + void testIsValidDataWithInvalidURI() throws Exception { + when(mockSubscriber.getType()).thenReturn(DataType.URI); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + URIRegisterDTO uriDTO = URIRegisterDTO.builder() + .contextPath("") + .rpcType("http") + .namespaceId("default") + .build(); + + Method method = RegisterServerConsumerExecutor.class.getDeclaredMethod("isValidData", Object.class); + method.setAccessible(true); + boolean result = (boolean) method.invoke(executor, uriDTO); + + assertFalse(result); + } + + @Test + void testIsValidDataWithValidMetaData() throws Exception { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + MetaDataRegisterDTO metaDTO = MetaDataRegisterDTO.builder() + .appName("testApp") + .path("/test/path") + .ruleName("testRule") + .rpcType("http") + .namespaceId("default") + .build(); + + Method method = RegisterServerConsumerExecutor.class.getDeclaredMethod("isValidData", Object.class); + method.setAccessible(true); + boolean result = (boolean) method.invoke(executor, metaDTO); + + assertTrue(result); + } + + @Test + void testIsValidDataWithInvalidMetaData() throws Exception { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + MetaDataRegisterDTO metaDTO = MetaDataRegisterDTO.builder() + .appName("") + .path("/test/path") + .ruleName("testRule") + .rpcType("http") + .namespaceId("default") + .build(); + + Method method = RegisterServerConsumerExecutor.class.getDeclaredMethod("isValidData", Object.class); + method.setAccessible(true); + boolean result = (boolean) method.invoke(executor, metaDTO); + + assertFalse(result); + } + + @Test + void testIsValidDataWithOtherType() throws Exception { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + DataTypeParent otherData = mock(DataTypeParent.class); + + Method method = RegisterServerConsumerExecutor.class.getDeclaredMethod("isValidData", Object.class); + method.setAccessible(true); + boolean result = (boolean) method.invoke(executor, otherData); + + assertTrue(result); + } + + @Test + void testSelectExecutorWithEmptyList() throws Exception { + when(mockSubscriber.getType()).thenReturn(DataType.META_DATA); + factory.addSubscribers(mockSubscriber); + RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + + Method method = RegisterServerConsumerExecutor.class.getDeclaredMethod("selectExecutor", Collection.class); + method.setAccessible(true); + + assertThrows(Exception.class, () -> { + method.invoke(executor, Collections.emptyList()); + }); + } + + @Test + void testRunWithMixedValidAndInvalidData() { + when(mockSubscriber.getType()).thenReturn(DataType.URI); + factory.addSubscribers(mockSubscriber); + + URIRegisterDTO validURI = URIRegisterDTO.builder() + .contextPath("/test") + .rpcType("http") + .namespaceId("default") + .build(); + + URIRegisterDTO invalidURI = URIRegisterDTO.builder() + .contextPath("") + .rpcType("http") + .namespaceId("default") + .build(); + + List data = new ArrayList<>(); + data.add(validURI); + data.add(invalidURI); + + final RegisterServerConsumerExecutor executor = (RegisterServerConsumerExecutor) factory.create(); + executor.setData(data); + executor.run(); + + verify(mockSubscriber, times(1)).executor(any()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/subscriber/ApiDocExecutorSubscriberTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/subscriber/ApiDocExecutorSubscriberTest.java new file mode 100644 index 000000000000..cfd54bdff7b2 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/subscriber/ApiDocExecutorSubscriberTest.java @@ -0,0 +1,177 @@ +/* + * 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 org.apache.shenyu.admin.disruptor.subscriber; + +import org.apache.shenyu.admin.service.register.ShenyuClientRegisterService; +import org.apache.shenyu.register.common.dto.ApiDocRegisterDTO; +import org.apache.shenyu.register.common.type.DataType; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; + +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.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test cases for {@link ApiDocExecutorSubscriber}. + */ +@ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) +class ApiDocExecutorSubscriberTest { + + @InjectMocks + private ApiDocExecutorSubscriber apiDocExecutorSubscriber; + + @Mock + private Map shenyuClientRegisterService; + + @Test + void testGetType() { + assertEquals(DataType.API_DOC, apiDocExecutorSubscriber.getType()); + } + + @Test + void testExecutorWithEmptyList() { + List list = new ArrayList<>(); + apiDocExecutorSubscriber.executor(list); + assertTrue(list.isEmpty()); + } + + @Test + void testExecutorWithValidData() { + List list = new ArrayList<>(); + ApiDocRegisterDTO apiDoc = ApiDocRegisterDTO.builder() + .contextPath("/test") + .apiPath("/api/test") + .httpMethod(0) + .rpcType("http") + .build(); + list.add(apiDoc); + + ShenyuClientRegisterService service = mock(ShenyuClientRegisterService.class); + when(shenyuClientRegisterService.get("http")).thenReturn(service); + + apiDocExecutorSubscriber.executor(list); + + verify(service, times(1)).registerApiDoc(any(ApiDocRegisterDTO.class)); + } + + @Test + void testExecutorWithMultipleData() { + List list = new ArrayList<>(); + ApiDocRegisterDTO apiDoc1 = ApiDocRegisterDTO.builder() + .contextPath("/test1") + .apiPath("/api/test1") + .httpMethod(0) + .rpcType("http") + .build(); + ApiDocRegisterDTO apiDoc2 = ApiDocRegisterDTO.builder() + .contextPath("/test2") + .apiPath("/api/test2") + .httpMethod(2) + .rpcType("http") + .build(); + list.add(apiDoc1); + list.add(apiDoc2); + + ShenyuClientRegisterService service = mock(ShenyuClientRegisterService.class); + when(shenyuClientRegisterService.get("http")).thenReturn(service); + + apiDocExecutorSubscriber.executor(list); + + verify(service, times(2)).registerApiDoc(any(ApiDocRegisterDTO.class)); + } + + @Test + void testExecutorWithNullService() { + List list = new ArrayList<>(); + ApiDocRegisterDTO apiDoc = ApiDocRegisterDTO.builder() + .contextPath("/test") + .apiPath("/api/test") + .httpMethod(0) + .rpcType("http") + .build(); + list.add(apiDoc); + + when(shenyuClientRegisterService.get("http")).thenReturn(null); + + apiDocExecutorSubscriber.executor(list); + + verify(shenyuClientRegisterService, times(1)).get("http"); + } + + @Test + void testExecutorWithDifferentRpcTypes() { + List list = new ArrayList<>(); + ApiDocRegisterDTO httpDoc = ApiDocRegisterDTO.builder() + .contextPath("/test1") + .apiPath("/api/test1") + .httpMethod(0) + .rpcType("http") + .build(); + ApiDocRegisterDTO dubboDoc = ApiDocRegisterDTO.builder() + .contextPath("/test2") + .apiPath("/api/test2") + .httpMethod(2) + .rpcType("dubbo") + .build(); + list.add(httpDoc); + list.add(dubboDoc); + + ShenyuClientRegisterService httpService = mock(ShenyuClientRegisterService.class); + ShenyuClientRegisterService dubboService = mock(ShenyuClientRegisterService.class); + when(shenyuClientRegisterService.get("http")).thenReturn(httpService); + when(shenyuClientRegisterService.get("dubbo")).thenReturn(dubboService); + + apiDocExecutorSubscriber.executor(list); + + verify(httpService, times(1)).registerApiDoc(any(ApiDocRegisterDTO.class)); + verify(dubboService, times(1)).registerApiDoc(any(ApiDocRegisterDTO.class)); + } + + @Test + void testExecutorWithUnknownRpcType() { + List list = new ArrayList<>(); + ApiDocRegisterDTO apiDoc = ApiDocRegisterDTO.builder() + .contextPath("/test") + .apiPath("/api/test") + .httpMethod(0) + .rpcType("unknown") + .build(); + list.add(apiDoc); + + when(shenyuClientRegisterService.get("unknown")).thenReturn(null); + + apiDocExecutorSubscriber.executor(list); + + verify(shenyuClientRegisterService, times(1)).get("unknown"); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/subscriber/DiscoveryConfigRegisterExecutorSubscriberTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/subscriber/DiscoveryConfigRegisterExecutorSubscriberTest.java new file mode 100644 index 000000000000..b59dbc0454d7 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/disruptor/subscriber/DiscoveryConfigRegisterExecutorSubscriberTest.java @@ -0,0 +1,118 @@ +/* + * 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 org.apache.shenyu.admin.disruptor.subscriber; + +import org.apache.shenyu.admin.service.DiscoveryService; +import org.apache.shenyu.register.common.dto.DiscoveryConfigRegisterDTO; +import org.apache.shenyu.register.common.type.DataType; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.time.Duration; +import java.util.ArrayList; +import java.util.List; + +import static org.awaitility.Awaitility.await; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test cases for {@link DiscoveryConfigRegisterExecutorSubscriber}. + */ +@ExtendWith(MockitoExtension.class) +class DiscoveryConfigRegisterExecutorSubscriberTest { + + @Mock + private DiscoveryService discoveryService; + + private DiscoveryConfigRegisterExecutorSubscriber subscriber; + + @BeforeEach + void setUp() { + subscriber = new DiscoveryConfigRegisterExecutorSubscriber(discoveryService); + } + + @Test + void testGetType() { + assertEquals(DataType.DISCOVERY_CONFIG, subscriber.getType()); + } + + @Test + void testExecutorWithEmptyList() { + List list = new ArrayList<>(); + subscriber.executor(list); + + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> verify(discoveryService, times(0)).registerDiscoveryConfig(any())); + } + + @Test + void testExecutorWithSingleData() { + List list = new ArrayList<>(); + DiscoveryConfigRegisterDTO dto = new DiscoveryConfigRegisterDTO(); + dto.setName("test-discovery"); + dto.setDiscoveryType("eureka"); + list.add(dto); + + subscriber.executor(list); + + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> verify(discoveryService, times(1)).registerDiscoveryConfig(any(DiscoveryConfigRegisterDTO.class))); + } + + @Test + void testExecutorWithMultipleData() { + List list = new ArrayList<>(); + DiscoveryConfigRegisterDTO dto1 = new DiscoveryConfigRegisterDTO(); + dto1.setName("test-discovery-1"); + dto1.setDiscoveryType("eureka"); + list.add(dto1); + + DiscoveryConfigRegisterDTO dto2 = new DiscoveryConfigRegisterDTO(); + dto2.setName("test-discovery-2"); + dto2.setDiscoveryType("nacos"); + list.add(dto2); + + subscriber.executor(list); + + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> verify(discoveryService, times(2)).registerDiscoveryConfig(any(DiscoveryConfigRegisterDTO.class))); + } + + @Test + void testExecutorWithException() { + List list = new ArrayList<>(); + DiscoveryConfigRegisterDTO dto = new DiscoveryConfigRegisterDTO(); + dto.setName("test-discovery"); + dto.setDiscoveryType("eureka"); + list.add(dto); + + doThrow(new RuntimeException("Test exception")).when(discoveryService).registerDiscoveryConfig(any()); + + subscriber.executor(list); + + await().atMost(Duration.ofSeconds(5)) + .untilAsserted(() -> verify(discoveryService, times(1)).registerDiscoveryConfig(any(DiscoveryConfigRegisterDTO.class))); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/I18nExceptionTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/I18nExceptionTest.java new file mode 100644 index 000000000000..b334665713a6 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/I18nExceptionTest.java @@ -0,0 +1,87 @@ +/* + * 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 org.apache.shenyu.admin.exception; + +import org.junit.jupiter.api.Test; + +import java.util.Locale; + +import static org.junit.jupiter.api.Assertions.assertArrayEquals; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Test case for {@link I18nException}. + */ +public class I18nExceptionTest { + + @Test + void testConstructorWithThrowable() { + Throwable cause = new RuntimeException("test cause"); + I18nException exception = new I18nException(cause); + + assertSame(cause, exception.getCause()); + assertEquals(Locale.getDefault(), exception.getLocale()); + assertNull(exception.getArgs()); + } + + @Test + void testConstructorWithLocaleMessageAndArgs() { + Locale locale = Locale.US; + String message = "Test message"; + Object[] args = new Object[]{"arg1", "arg2"}; + + I18nException exception = new I18nException(locale, message, args); + + assertEquals(message, exception.getMessage()); + assertEquals(locale, exception.getLocale()); + assertArrayEquals(args, exception.getArgs()); + } + + @Test + void testConstructorWithLocaleMessageThrowableAndArgs() { + Locale locale = Locale.FRANCE; + String message = "Test message with cause"; + Throwable cause = new IllegalArgumentException("test cause"); + Object[] args = new Object[]{"arg1"}; + + I18nException exception = new I18nException(locale, message, cause, args); + + assertEquals(message, exception.getMessage()); + assertSame(cause, exception.getCause()); + assertEquals(locale, exception.getLocale()); + assertArrayEquals(args, exception.getArgs()); + } + + @Test + void testGetLocale() { + Locale locale = Locale.CHINA; + I18nException exception = new I18nException(locale, "message"); + + assertEquals(locale, exception.getLocale()); + } + + @Test + void testGetArgs() { + Object[] args = new Object[]{"test", 123}; + I18nException exception = new I18nException(Locale.getDefault(), "message", args); + + assertArrayEquals(args, exception.getArgs()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ResourceNotFoundExceptionTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ResourceNotFoundExceptionTest.java new file mode 100644 index 000000000000..b1e32439486f --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ResourceNotFoundExceptionTest.java @@ -0,0 +1,58 @@ +/* + * 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 org.apache.shenyu.admin.exception; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.Assertions; + +/** + * Test case for {@link ResourceNotFoundException}. + */ +public class ResourceNotFoundExceptionTest { + + @Test + void testConstructorWithThrowable() { + Throwable cause = new RuntimeException("test cause"); + ResourceNotFoundException exception = new ResourceNotFoundException(cause); + + Assertions.assertSame(cause, exception.getCause()); + Assertions.assertThrows(RuntimeException.class, () -> { + throw exception; + }); + } + + @Test + void testConstructorWithMessage() { + String message = "Resource not found"; + ResourceNotFoundException exception = new ResourceNotFoundException(message); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertNull(exception.getCause()); + } + + @Test + void testConstructorWithMessageAndThrowable() { + String message = "Resource not found with cause"; + Throwable cause = new IllegalArgumentException("test cause"); + ResourceNotFoundException exception = new ResourceNotFoundException(message, cause); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertSame(cause, exception.getCause()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ShenyuAdminExceptionTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ShenyuAdminExceptionTest.java new file mode 100644 index 000000000000..4e2734a8f55e --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ShenyuAdminExceptionTest.java @@ -0,0 +1,58 @@ +/* + * 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 org.apache.shenyu.admin.exception; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.Assertions; + +/** + * Test case for {@link ShenyuAdminException}. + */ +public class ShenyuAdminExceptionTest { + + @Test + void testConstructorWithThrowable() { + Throwable cause = new RuntimeException("test cause"); + ShenyuAdminException exception = new ShenyuAdminException(cause); + + Assertions.assertSame(cause, exception.getCause()); + Assertions.assertThrows(RuntimeException.class, () -> { + throw exception; + }); + } + + @Test + void testConstructorWithMessage() { + String message = "Shenyu admin exception"; + ShenyuAdminException exception = new ShenyuAdminException(message); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertNull(exception.getCause()); + } + + @Test + void testConstructorWithMessageAndThrowable() { + String message = "Shenyu admin exception with cause"; + Throwable cause = new IllegalArgumentException("test cause"); + ShenyuAdminException exception = new ShenyuAdminException(message, cause); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertSame(cause, exception.getCause()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ValidFailExceptionTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ValidFailExceptionTest.java new file mode 100644 index 000000000000..d31a9931a3fe --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/ValidFailExceptionTest.java @@ -0,0 +1,58 @@ +/* + * 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 org.apache.shenyu.admin.exception; + +import org.junit.jupiter.api.Test; + +import org.junit.jupiter.api.Assertions; + +/** + * Test case for {@link ValidFailException}. + */ +public class ValidFailExceptionTest { + + @Test + void testConstructorWithThrowable() { + Throwable cause = new RuntimeException("test cause"); + ValidFailException exception = new ValidFailException(cause); + + Assertions.assertSame(cause, exception.getCause()); + Assertions.assertThrows(RuntimeException.class, () -> { + throw exception; + }); + } + + @Test + void testConstructorWithMessage() { + String message = "Validation failed"; + ValidFailException exception = new ValidFailException(message); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertNull(exception.getCause()); + } + + @Test + void testConstructorWithMessageAndThrowable() { + String message = "Validation failed with cause"; + Throwable cause = new IllegalArgumentException("test cause"); + ValidFailException exception = new ValidFailException(message, cause); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertSame(cause, exception.getCause()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/WebI18nExceptionTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/WebI18nExceptionTest.java new file mode 100644 index 000000000000..2f2adf907d62 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/exception/WebI18nExceptionTest.java @@ -0,0 +1,96 @@ +/* + * 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 org.apache.shenyu.admin.exception; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.i18n.LocaleContextHolder; + +import java.util.Locale; + +import org.junit.jupiter.api.Assertions; + +/** + * Test case for {@link WebI18nException}. + */ +@ExtendWith(MockitoExtension.class) +public class WebI18nExceptionTest { + + @Test + void testConstructorWithThrowable() { + Throwable cause = new RuntimeException("test cause"); + WebI18nException exception = new WebI18nException(cause); + + Assertions.assertSame(cause, exception.getCause()); + Assertions.assertEquals(Locale.getDefault(), exception.getLocale()); + Assertions.assertNull(exception.getArgs()); + } + + @Test + void testConstructorWithMessage() { + Locale locale = Locale.US; + String message = "Test message"; + + try (MockedStatic localeContextHolderMock = org.mockito.Mockito.mockStatic(LocaleContextHolder.class)) { + localeContextHolderMock.when(LocaleContextHolder::getLocale).thenReturn(locale); + + WebI18nException exception = new WebI18nException(message); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(locale, exception.getLocale()); + Assertions.assertInstanceOf(Object[].class, exception.getArgs()); + } + } + + @Test + void testConstructorWithMessageAndArgs() { + Locale locale = Locale.FRANCE; + String message = "Test message with args"; + Object[] args = new Object[]{"arg1", "arg2"}; + + try (MockedStatic localeContextHolderMock = org.mockito.Mockito.mockStatic(LocaleContextHolder.class)) { + localeContextHolderMock.when(LocaleContextHolder::getLocale).thenReturn(locale); + + WebI18nException exception = new WebI18nException(message, args); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertEquals(locale, exception.getLocale()); + Assertions.assertArrayEquals(args, exception.getArgs()); + } + } + + @Test + void testConstructorWithMessageAndThrowable() { + Locale locale = Locale.CHINA; + String message = "Test message with cause"; + Throwable cause = new IllegalArgumentException("test cause"); + + try (MockedStatic localeContextHolderMock = org.mockito.Mockito.mockStatic(LocaleContextHolder.class)) { + localeContextHolderMock.when(LocaleContextHolder::getLocale).thenReturn(locale); + + WebI18nException exception = new WebI18nException(message, cause); + + Assertions.assertEquals(message, exception.getMessage()); + Assertions.assertSame(cause, exception.getCause()); + Assertions.assertEquals(locale, exception.getLocale()); + Assertions.assertInstanceOf(Object[].class, exception.getArgs()); + } + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/ApplicationStartListenerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/ApplicationStartListenerTest.java index 0b9fbd389934..935867fecc66 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/ApplicationStartListenerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/ApplicationStartListenerTest.java @@ -17,18 +17,74 @@ package org.apache.shenyu.admin.listener; -import org.apache.shenyu.admin.AbstractSpringIntegrationTest; +import org.apache.shenyu.admin.mode.ShenyuRunningModeService; +import org.apache.shenyu.common.utils.IpUtils; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.boot.web.server.WebServer; +import org.springframework.boot.web.context.WebServerInitializedEvent; +import org.springframework.test.util.ReflectionTestUtils; + +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; /** * Test case for {@link ApplicationStartListener}. */ -public final class ApplicationStartListenerTest extends AbstractSpringIntegrationTest { +@ExtendWith(MockitoExtension.class) +public final class ApplicationStartListenerTest { + + @InjectMocks + private ApplicationStartListener applicationStartListener; + + @Mock + private ShenyuRunningModeService shenyuRunningModeService; + + @Mock + private WebServerInitializedEvent event; + + @Mock + private WebServer webServer; + + @BeforeEach + void setUp() { + ReflectionTestUtils.setField(applicationStartListener, "contextPath", "/shenyu"); + when(event.getWebServer()).thenReturn(webServer); + when(webServer.getPort()).thenReturn(9095); + } + + @Test + void testOnApplicationEvent() { + try (MockedStatic ipUtilsMocked = mockStatic(IpUtils.class)) { + ipUtilsMocked.when(IpUtils::getHost).thenReturn("192.168.1.1"); + applicationStartListener.onApplicationEvent(event); + verify(shenyuRunningModeService).start("192.168.1.1", 9095, "/shenyu"); + } + } + + @Test + void testOnApplicationEventWithEmptyContextPath() { + ReflectionTestUtils.setField(applicationStartListener, "contextPath", ""); + try (MockedStatic ipUtilsMocked = mockStatic(IpUtils.class)) { + ipUtilsMocked.when(IpUtils::getHost).thenReturn("10.0.0.1"); + applicationStartListener.onApplicationEvent(event); + verify(shenyuRunningModeService).start("10.0.0.1", 9095, ""); + } + } @Test - public void testOnApplicationEvent() { - // ApplicationStartListener is automatically triggered during Spring Boot startup - // This test verifies that the application context loads successfully - // and the listener is registered and executed without errors + void testOnApplicationEventWithDifferentPort() { + when(webServer.getPort()).thenReturn(8080); + try (MockedStatic ipUtilsMocked = mockStatic(IpUtils.class)) { + ipUtilsMocked.when(IpUtils::getHost).thenReturn("127.0.0.1"); + applicationStartListener.onApplicationEvent(event); + verify(shenyuRunningModeService).start("127.0.0.1", 8080, "/shenyu"); + } } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java index 190f44536f59..fad86580f1cf 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/DataChangedEventDispatcherTest.java @@ -31,11 +31,14 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; import org.springframework.context.ApplicationContext; import org.springframework.test.util.ReflectionTestUtils; import java.lang.reflect.Field; import java.util.ArrayList; +import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,6 +48,8 @@ import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -53,6 +58,7 @@ * Test cases for {@link DataChangedEventDispatcher}. */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public final class DataChangedEventDispatcherTest { @InjectMocks @@ -213,4 +219,96 @@ public void afterPropertiesSetTest() { assertTrue(listeners.contains(websocketDataChangedListener)); assertTrue(listeners.contains(zookeeperDataChangedListener)); } + + /** + * onApplicationEvent PROXY_SELECTOR configGroupEnum test case. + */ + @Test + void onApplicationEventWithProxySelectorConfigGroupTest() { + when(clusterProperties.isEnabled()).thenReturn(true); + when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true); + DataChangedEvent dataChangedEvent = new DataChangedEvent(ConfigGroupEnum.PROXY_SELECTOR, null, new ArrayList<>()); + dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent); + verify(httpLongPollingDataChangedListener, times(1)).onProxySelectorChanged(anyList(), any()); + verify(nacosDataChangedListener, times(1)).onProxySelectorChanged(anyList(), any()); + verify(websocketDataChangedListener, times(1)).onProxySelectorChanged(anyList(), any()); + verify(zookeeperDataChangedListener, times(1)).onProxySelectorChanged(anyList(), any()); + } + + /** + * onApplicationEvent AI_PROXY_API_KEY configGroupEnum test case. + */ + @Test + void onApplicationEventWithAiProxyApiKeyConfigGroupTest() { + when(clusterProperties.isEnabled()).thenReturn(true); + when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true); + DataChangedEvent dataChangedEvent = new DataChangedEvent(ConfigGroupEnum.AI_PROXY_API_KEY, null, new ArrayList<>()); + dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent); + verify(httpLongPollingDataChangedListener, times(1)).onAiProxyApiKeyChanged(anyList(), any()); + verify(nacosDataChangedListener, times(1)).onAiProxyApiKeyChanged(anyList(), any()); + verify(websocketDataChangedListener, times(1)).onAiProxyApiKeyChanged(anyList(), any()); + verify(zookeeperDataChangedListener, times(1)).onAiProxyApiKeyChanged(anyList(), any()); + } + + /** + * onApplicationEvent DISCOVER_UPSTREAM configGroupEnum test case — also triggers loadDocOnUpstreamChanged. + */ + @Test + void onApplicationEventWithDiscoverUpstreamConfigGroupTest() { + when(clusterProperties.isEnabled()).thenReturn(true); + when(shenyuClusterSelectMasterService.isMaster()).thenReturn(true); + DataChangedEvent dataChangedEvent = new DataChangedEvent(ConfigGroupEnum.DISCOVER_UPSTREAM, null, new ArrayList<>()); + dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent); + verify(httpLongPollingDataChangedListener, times(1)).onDiscoveryUpstreamChanged(anyList(), any()); + verify(nacosDataChangedListener, times(1)).onDiscoveryUpstreamChanged(anyList(), any()); + verify(websocketDataChangedListener, times(1)).onDiscoveryUpstreamChanged(anyList(), any()); + verify(zookeeperDataChangedListener, times(1)).onDiscoveryUpstreamChanged(anyList(), any()); + verify(loadServiceDocEntry, atLeastOnce()).loadDocOnUpstreamChanged(anyList(), any()); + } + + /** + * When cluster is disabled, all listeners receive the event regardless of master status. + */ + @Test + void onApplicationEventClusterDisabledAllListenersNotifiedTest() { + when(clusterProperties.isEnabled()).thenReturn(false); + DataChangedEvent dataChangedEvent = new DataChangedEvent(ConfigGroupEnum.PLUGIN, null, new ArrayList<>()); + dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent); + verify(httpLongPollingDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + verify(nacosDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + verify(websocketDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + verify(zookeeperDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + } + + /** + * When cluster enabled and not master, non-AbstractDataChangedListener is skipped. + */ + @Test + void onApplicationEventNotMasterSkipsNonAbstractListenersTest() { + when(clusterProperties.isEnabled()).thenReturn(true); + when(shenyuClusterSelectMasterService.isMaster()).thenReturn(false); + List orderedListeners = new ArrayList<>(); + orderedListeners.add(nacosDataChangedListener); + ReflectionTestUtils.setField(dataChangedEventDispatcher, "listeners", Collections.unmodifiableList(orderedListeners)); + DataChangedEvent dataChangedEvent = new DataChangedEvent(ConfigGroupEnum.PLUGIN, null, new ArrayList<>()); + dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent); + verify(nacosDataChangedListener, never()).onPluginChanged(anyList(), any()); + } + + /** + * When shenyuClusterSelectMasterService is null, all listeners still receive the event. + */ + @Test + void onApplicationEventNullMasterServiceDispatchesAllTest() throws NoSuchFieldException, IllegalAccessException { + when(clusterProperties.isEnabled()).thenReturn(true); + Field field = DataChangedEventDispatcher.class.getDeclaredField("shenyuClusterSelectMasterService"); + field.setAccessible(true); + field.set(dataChangedEventDispatcher, null); + DataChangedEvent dataChangedEvent = new DataChangedEvent(ConfigGroupEnum.PLUGIN, null, new ArrayList<>()); + dataChangedEventDispatcher.onApplicationEvent(dataChangedEvent); + verify(nacosDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + verify(httpLongPollingDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + verify(websocketDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + verify(zookeeperDataChangedListener, times(1)).onPluginChanged(anyList(), any()); + } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java index 6318623ecccc..8fbd127453ce 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/http/HttpLongPollingDataChangedListenerTest.java @@ -17,45 +17,89 @@ package org.apache.shenyu.admin.listener.http; -import org.apache.commons.lang3.StringUtils; +import jakarta.servlet.AsyncContext; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.shenyu.admin.config.properties.HttpSyncProperties; +import org.apache.shenyu.admin.listener.AbstractDataChangedListener; +import org.apache.shenyu.admin.listener.ConfigDataCache; import org.apache.shenyu.admin.model.result.ShenyuAdminResult; +import org.apache.shenyu.admin.service.publish.InstanceInfoReportEventPublisher; +import org.apache.shenyu.admin.spring.SpringBeanUtils; import org.apache.shenyu.admin.utils.ShenyuResultMessage; +import org.apache.shenyu.common.dto.AppAuthData; +import org.apache.shenyu.common.dto.DiscoverySyncData; +import org.apache.shenyu.common.dto.MetaData; +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.ProxySelectorData; +import org.apache.shenyu.common.dto.RuleData; +import org.apache.shenyu.common.dto.SelectorData; import org.apache.shenyu.common.enums.ConfigGroupEnum; +import org.apache.shenyu.common.enums.DataEventTypeEnum; import org.apache.shenyu.common.utils.GsonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.mockito.junit.jupiter.MockitoSettings; +import org.mockito.quality.Strictness; +import org.springframework.context.ApplicationContext; import org.springframework.http.MediaType; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import java.io.UnsupportedEncodingException; +import java.lang.reflect.Field; +import java.lang.reflect.Method; +import java.time.Duration; import java.util.ArrayList; import java.util.List; import java.util.Objects; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; +import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.ConcurrentMap; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; /** * The TestCase for {@link HttpLongPollingDataChangedListener}. */ @ExtendWith(MockitoExtension.class) +@MockitoSettings(strictness = Strictness.LENIENT) public final class HttpLongPollingDataChangedListenerTest { private static final String X_REAL_IP = "X-Real-IP"; private static final String X_FORWARDED_FOR = "X-Forwarded-For"; + private static final String DEFAULT_NAMESPACE = "649330b6-c2d7-4edc-be8e-8a54df9eb385"; + + @Mock + private HttpSyncProperties httpSyncProperties; + + private HttpLongPollingDataChangedListener listener; + private MockHttpServletRequest httpServletRequest; private MockHttpServletResponse httpServletResponse; + @Mock + private InstanceInfoReportEventPublisher instanceInfoReportEventPublisher; + + @Mock + private ApplicationContext applicationContext; + @BeforeEach public void setUp() { + Mockito.when(httpSyncProperties.getRefreshInterval()).thenReturn(Duration.ofSeconds(30)); + Mockito.when(httpSyncProperties.getNotifyBatchSize()).thenReturn(100); + Mockito.when(applicationContext.getBean(InstanceInfoReportEventPublisher.class)) + .thenReturn(instanceInfoReportEventPublisher); + SpringBeanUtils.getInstance().setApplicationContext(applicationContext); + listener = new HttpLongPollingDataChangedListener(httpSyncProperties); this.httpServletRequest = new MockHttpServletRequest(); this.httpServletResponse = new MockHttpServletResponse(); } @@ -63,22 +107,22 @@ public void setUp() { /** * test DoLongPolling Process. * - * @throws UnsupportedEncodingException throw not support encoding + * @throws Exception throw exception */ @Test - public void testDoLongPolling() throws UnsupportedEncodingException { + public void testDoLongPolling() throws Exception { testCompareChangedGroup(); testGetRemoteIp(); testGenerateResponse(); } /** - * test GenerateResponse. + * test GenerateResponse Manual. * * @throws UnsupportedEncodingException throw not support encoding */ @Test - public void testGenerateResponse() throws UnsupportedEncodingException { + public void testGenerateResponseManual() throws UnsupportedEncodingException { List changedGroups = new ArrayList<>(); changedGroups.add(ConfigGroupEnum.PLUGIN); this.httpServletResponse.setHeader("Pragma", "no-cache"); @@ -89,18 +133,36 @@ public void testGenerateResponse() throws UnsupportedEncodingException { this.httpServletResponse.getWriter().println(GsonUtils.getInstance().toJson(ShenyuAdminResult.success(ShenyuResultMessage.SUCCESS, changedGroups))); } + /** + * test generateResponse. + */ + @Test + public void testGenerateResponse() throws Exception { + List changedGroups = List.of(ConfigGroupEnum.PLUGIN); + invokeGenerateResponse(this.httpServletResponse, changedGroups); + assertEquals("application/json", this.httpServletResponse.getContentType()); + assertEquals(200, this.httpServletResponse.getStatus()); + } + /** * test getRemoteIp. */ @Test - public void testGetRemoteIp() { + public void testGetRemoteIp() throws Exception { this.httpServletRequest.addHeader(X_FORWARDED_FOR, "x-forwarded-for,test"); this.httpServletRequest.addHeader(X_REAL_IP, "127.0.0.1"); - String xForwardedFor = httpServletRequest.getHeader(X_FORWARDED_FOR); - assertNotNull(xForwardedFor); - assertTrue(StringUtils.isNotBlank(xForwardedFor)); - assertEquals("x-forwarded-for,test", xForwardedFor); - assertEquals("127.0.0.1", httpServletRequest.getHeader(X_REAL_IP)); + String remoteIp = invokeGetRemoteIp(this.httpServletRequest); + assertEquals("x-forwarded-for", remoteIp); + + this.httpServletRequest = new MockHttpServletRequest(); + this.httpServletRequest.addHeader(X_REAL_IP, "127.0.0.1"); + remoteIp = invokeGetRemoteIp(this.httpServletRequest); + assertEquals("127.0.0.1", remoteIp); + + this.httpServletRequest = new MockHttpServletRequest(); + this.httpServletRequest.setRemoteAddr("192.168.1.1"); + remoteIp = invokeGetRemoteIp(this.httpServletRequest); + assertEquals("192.168.1.1", remoteIp); } /** @@ -130,4 +192,598 @@ public void testCompareChangedGroup() { assertEquals(2, params.length); } } -} + + /** + * test getNamespaceId. + */ + @Test + public void testGetNamespaceId() throws Exception { + String namespaceId = invokeGetNamespaceId(this.httpServletRequest); + assertEquals("649330b6-c2d7-4edc-be8e-8a54df9eb385", namespaceId); + + this.httpServletRequest.setParameter("namespaceId", "testNamespace"); + namespaceId = invokeGetNamespaceId(this.httpServletRequest); + assertEquals("testNamespace", namespaceId); + } + + /** + * test buildCacheKey. + */ + @Test + public void testBuildCacheKey() { + String key = HttpLongPollingDataChangedListener.buildCacheKey("namespace", "group"); + assertEquals("namespace_group", key); + } + + /** + * test Constructor. + */ + @Test + public void testConstructor() { + assertNotNull(listener); + try { + Field schedulerField = HttpLongPollingDataChangedListener.class.getDeclaredField("scheduler"); + schedulerField.setAccessible(true); + Object scheduler = schedulerField.get(listener); + assertNotNull(scheduler); + } catch (Exception e) { + // expected + } + } + + /** + * test compareChangedGroup with invalid params (null param triggers exception). + */ + @Test + public void testCompareChangedGroupInvalidParams() throws Exception { + // Only set PLUGIN with invalid value; all other group params are null → first null triggers exception + this.httpServletRequest.setParameter(ConfigGroupEnum.PLUGIN.name(), "invalid"); + try { + invokeCompareChangedGroup(this.httpServletRequest); + } catch (Exception e) { + assertNotNull(e.getCause()); + // The first group without a valid param will trigger the exception + assertNotNull(e.getCause().getMessage()); + } + } + + /** + * test compareChangedGroup with one group having invalid format. + */ + @Test + public void testCompareChangedGroupSpecificInvalidParam() throws Exception { + // Populate all groups in CACHE first so they don't NPE + populateAllGroupsInCache(DEFAULT_NAMESPACE, "md5"); + setAllGroupParamsDefault(this.httpServletRequest); + // Override PLUGIN with invalid value (only one comma-part) + this.httpServletRequest.setParameter(ConfigGroupEnum.PLUGIN.name(), "invalid"); + try { + invokeCompareChangedGroup(this.httpServletRequest); + } catch (Exception e) { + assertNotNull(e.getCause()); + assertEquals("group param invalid:invalid", e.getCause().getMessage()); + } + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test checkCacheDelayAndUpdate - same md5 returns false. + */ + @Test + public void testCheckCacheDelayAndUpdateSameMd5() throws Exception { + String namespaceId = DEFAULT_NAMESPACE; + String group = ConfigGroupEnum.PLUGIN.name(); + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group); + ConfigDataCache cache = new ConfigDataCache(group, "{}", "md5", 1000L, namespaceId); + getCache().put(cacheKey, cache); + + boolean result = invokeCheckCacheDelayAndUpdate(cache, "md5", 500L); + assertEquals(false, result); + + getCache().remove(cacheKey); + } + + /** + * test checkCacheDelayAndUpdate - different md5 but client is newer and cache is concurrently updated. + * When latest == serverCache and clientModifyTime > lastModifyTime, enters sync block; + * to avoid calling refreshLocalCache (which needs Spring), put a different object in CACHE so latest != serverCache. + */ + @Test + public void testCheckCacheDelayAndUpdateClientNewer() throws Exception { + String namespaceId = DEFAULT_NAMESPACE; + String group = ConfigGroupEnum.PLUGIN.name(); + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group); + + // serverCache is NOT the object in CACHE; CACHE has a different instance with same md5 as client + ConfigDataCache serverCache = new ConfigDataCache(group, "{}", "md5", 1000L, namespaceId); + ConfigDataCache latestCache = new ConfigDataCache(group, "{}", "clientMd5", 2000L, namespaceId); + getCache().put(cacheKey, latestCache); + + // clientMd5 matches latestCache.md5 → returns false (no update needed) + boolean result = invokeCheckCacheDelayAndUpdate(serverCache, "clientMd5", 2000L); + assertEquals(false, result); + + getCache().remove(cacheKey); + } + + /** + * test checkCacheDelayAndUpdate - different md5, server is newer. + */ + @Test + public void testCheckCacheDelayAndUpdateServerNewer() throws Exception { + String namespaceId = DEFAULT_NAMESPACE; + String group = ConfigGroupEnum.PLUGIN.name(); + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group); + ConfigDataCache cache = new ConfigDataCache(group, "{}", "md5", 1000L, namespaceId); + getCache().put(cacheKey, cache); + + boolean result = invokeCheckCacheDelayAndUpdate(cache, "newMd5", 500L); + assertEquals(true, result); + + getCache().remove(cacheKey); + } + + /** + * test checkCacheDelayAndUpdate when CACHE has been concurrently updated (latest != serverCache). + */ + @Test + public void testCheckCacheDelayAndUpdateLatestChanged() throws Exception { + String namespaceId = DEFAULT_NAMESPACE; + String group = ConfigGroupEnum.PLUGIN.name(); + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group); + + ConfigDataCache oldCache = new ConfigDataCache(group, "{}", "oldMd5", 500L, namespaceId); + ConfigDataCache newCache = new ConfigDataCache(group, "{}", "newMd5", 2000L, namespaceId); + getCache().put(cacheKey, newCache); + + // oldCache != CACHE.get(cacheKey), clientMd5 != newMd5 → true + boolean result = invokeCheckCacheDelayAndUpdate(oldCache, "clientMd5", 2000L); + assertEquals(true, result); + + // clientMd5 == newMd5 → false + result = invokeCheckCacheDelayAndUpdate(oldCache, "newMd5", 2000L); + assertEquals(false, result); + + getCache().remove(cacheKey); + } + + /** + * test doLongPolling with changed groups. + */ + @Test + public void testDoLongPollingWithChangedGroups() throws Exception { + String group = ConfigGroupEnum.PLUGIN.name(); + populateAllGroupsInCache(DEFAULT_NAMESPACE, "serverMd5"); + + setAllGroupParamsDefault(this.httpServletRequest); + // Override PLUGIN with different md5 and older time → changed group + this.httpServletRequest.setParameter(group, "clientMd5,500"); + + listener.doLongPolling(this.httpServletRequest, this.httpServletResponse); + + assertEquals(200, this.httpServletResponse.getStatus()); + + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test doLongPolling with client port triggering instance report. + */ + @Test + public void testDoLongPollingWithClientPort() throws Exception { + String group = ConfigGroupEnum.PLUGIN.name(); + populateAllGroupsInCache(DEFAULT_NAMESPACE, "serverMd5"); + + setAllGroupParamsDefault(this.httpServletRequest); + this.httpServletRequest.setParameter(group, "clientMd5,500"); + this.httpServletRequest.addHeader("X-Real-PORT", "8080"); + this.httpServletRequest.addHeader(X_REAL_IP, "10.0.0.1"); + + listener.doLongPolling(this.httpServletRequest, this.httpServletResponse); + + Mockito.verify(instanceInfoReportEventPublisher).publish(any()); + assertEquals(200, this.httpServletResponse.getStatus()); + + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test doLongPolling with no changed groups (async path). + */ + @Test + public void testDoLongPollingNoChangedGroups() throws Exception { + populateAllGroupsInCache(DEFAULT_NAMESPACE, "sameMd5"); + + // Use Mockito mock so we can stub startAsync + HttpServletRequest mockRequest = Mockito.mock(HttpServletRequest.class); + final AsyncContext asyncContext = Mockito.mock(AsyncContext.class); + + // Return matching md5 for all group params so no groups are changed + for (ConfigGroupEnum group : ConfigGroupEnum.values()) { + Mockito.when(mockRequest.getParameter(group.name())).thenReturn("sameMd5,500"); + } + Mockito.when(mockRequest.getParameter("namespaceId")).thenReturn(null); + Mockito.when(mockRequest.getHeader("X-Forwarded-For")).thenReturn(null); + Mockito.when(mockRequest.getHeader("X-Real-IP")).thenReturn(null); + Mockito.when(mockRequest.getRemoteAddr()).thenReturn("127.0.0.1"); + Mockito.when(mockRequest.getHeader("X-Real-PORT")).thenReturn(null); + Mockito.when(mockRequest.getHeader("bootstrapInstanceInfo")).thenReturn(null); + Mockito.when(mockRequest.startAsync()).thenReturn(asyncContext); + Mockito.when(asyncContext.getRequest()).thenReturn(mockRequest); + Mockito.when(asyncContext.getResponse()).thenReturn(this.httpServletResponse); + + listener.doLongPolling(mockRequest, this.httpServletResponse); + + Mockito.verify(mockRequest).startAsync(); + + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test doLongPolling with X-Forwarded-For header. + */ + @Test + public void testDoLongPollingWithXForwardedFor() throws Exception { + String group = ConfigGroupEnum.PLUGIN.name(); + populateAllGroupsInCache(DEFAULT_NAMESPACE, "serverMd5"); + + setAllGroupParamsDefault(this.httpServletRequest); + this.httpServletRequest.setParameter(group, "clientMd5,500"); + this.httpServletRequest.addHeader(X_FORWARDED_FOR, "192.168.1.100,10.0.0.1"); + + listener.doLongPolling(this.httpServletRequest, this.httpServletResponse); + + assertEquals(200, this.httpServletResponse.getStatus()); + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test doLongPolling with custom namespaceId parameter. + */ + @Test + public void testDoLongPollingWithNamespaceId() throws Exception { + String namespaceId = "customNamespace"; + final String group = ConfigGroupEnum.PLUGIN.name(); + populateAllGroupsInCache(namespaceId, "serverMd5"); + + this.httpServletRequest.setParameter("namespaceId", namespaceId); + setAllGroupParamsForNamespace(this.httpServletRequest, namespaceId); + this.httpServletRequest.setParameter(group, "clientMd5,500"); + + listener.doLongPolling(this.httpServletRequest, this.httpServletResponse); + + assertEquals(200, this.httpServletResponse.getStatus()); + clearAllGroupsInCache(namespaceId); + } + + /** + * test afterAppAuthChanged via reflection. + */ + @Test + public void testAfterAppAuthChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterAppAuthChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test afterMetaDataChanged via reflection. + */ + @Test + public void testAfterMetaDataChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterMetaDataChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test afterPluginChanged via reflection. + */ + @Test + public void testAfterPluginChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterPluginChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test afterRuleChanged via reflection. + */ + @Test + public void testAfterRuleChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterRuleChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test afterSelectorChanged via reflection. + */ + @Test + public void testAfterSelectorChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterSelectorChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test afterProxySelectorChanged via reflection. + */ + @Test + public void testAfterProxySelectorChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterProxySelectorChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test afterDiscoveryUpstreamDataChanged via reflection. + */ + @Test + public void testAfterDiscoveryUpstreamDataChanged() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "afterDiscoveryUpstreamDataChanged", List.class, DataEventTypeEnum.class, String.class); + method.setAccessible(true); + method.invoke(listener, new ArrayList(), DataEventTypeEnum.CREATE, DEFAULT_NAMESPACE); + } + + /** + * test DataChangeTask with empty clients (no-op). + */ + @Test + public void testDataChangeTaskEmptyClients() throws Exception { + Class dataChangeTaskClass = Class.forName("org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$DataChangeTask"); + java.lang.reflect.Constructor constructor = dataChangeTaskClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, ConfigGroupEnum.class, String.class); + constructor.setAccessible(true); + Object dataChangeTask = constructor.newInstance(listener, ConfigGroupEnum.PLUGIN, "testNamespace"); + + Method runMethod = dataChangeTaskClass.getMethod("run"); + runMethod.invoke(dataChangeTask); + // no exception = pass, clients map is empty for this namespace + } + + /** + * test DataChangeTask with clients in queue (below batch size). + */ + @Test + @SuppressWarnings("unchecked") + public void testDataChangeTaskWithClients() throws Exception { + final String namespaceId = "testNamespaceWithClients"; + + populateAllGroupsInCache(DEFAULT_NAMESPACE, "sameMd5"); + + AsyncContext asyncContext = Mockito.mock(AsyncContext.class); + MockHttpServletRequest req = new MockHttpServletRequest(); + MockHttpServletResponse resp = new MockHttpServletResponse(); + setAllGroupParamsDefault(req); + Mockito.when(asyncContext.getRequest()).thenReturn(req); + Mockito.when(asyncContext.getResponse()).thenReturn(resp); + + Class longPollingClientClass = Class.forName( + "org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$LongPollingClient"); + java.lang.reflect.Constructor clientCtor = longPollingClientClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, AsyncContext.class, String.class, long.class, String.class); + clientCtor.setAccessible(true); + Object client = clientCtor.newInstance(listener, asyncContext, "127.0.0.1", 1L, namespaceId); + + BlockingQueue queue = new ArrayBlockingQueue<>(1024); + queue.add(client); + + Field clientsMapField = HttpLongPollingDataChangedListener.class.getDeclaredField("clientsMap"); + clientsMapField.setAccessible(true); + java.util.Map> clientsMap = + (java.util.Map>) clientsMapField.get(listener); + clientsMap.put(namespaceId, queue); + + Class dataChangeTaskClass = Class.forName( + "org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$DataChangeTask"); + java.lang.reflect.Constructor taskCtor = dataChangeTaskClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, ConfigGroupEnum.class, String.class); + taskCtor.setAccessible(true); + Object dataChangeTask = taskCtor.newInstance(listener, ConfigGroupEnum.PLUGIN, namespaceId); + + Method runMethod = dataChangeTaskClass.getMethod("run"); + runMethod.invoke(dataChangeTask); + + Mockito.verify(asyncContext).complete(); + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test DataChangeTask batch notify path (clients > notifyBatchSize). + */ + @Test + @SuppressWarnings("unchecked") + public void testDataChangeTaskBatchNotify() throws Exception { + // notifyBatchSize = 1, add 3 clients → batch path + Mockito.when(httpSyncProperties.getNotifyBatchSize()).thenReturn(1); + final String namespaceId = "testNamespaceBatch"; + + populateAllGroupsInCache(DEFAULT_NAMESPACE, "sameMd5"); + + BlockingQueue queue = new ArrayBlockingQueue<>(1024); + Class longPollingClientClass = Class.forName( + "org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$LongPollingClient"); + java.lang.reflect.Constructor clientCtor = longPollingClientClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, AsyncContext.class, String.class, long.class, String.class); + clientCtor.setAccessible(true); + + for (int i = 0; i < 3; i++) { + AsyncContext asyncContext = Mockito.mock(AsyncContext.class); + MockHttpServletRequest req = new MockHttpServletRequest(); + MockHttpServletResponse resp = new MockHttpServletResponse(); + setAllGroupParamsDefault(req); + Mockito.when(asyncContext.getRequest()).thenReturn(req); + Mockito.when(asyncContext.getResponse()).thenReturn(resp); + Object client = clientCtor.newInstance(listener, asyncContext, "127.0.0." + i, 1L, namespaceId); + queue.add(client); + } + + Field clientsMapField = HttpLongPollingDataChangedListener.class.getDeclaredField("clientsMap"); + clientsMapField.setAccessible(true); + java.util.Map> clientsMap = + (java.util.Map>) clientsMapField.get(listener); + clientsMap.put(namespaceId, queue); + + Class dataChangeTaskClass = Class.forName( + "org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$DataChangeTask"); + java.lang.reflect.Constructor taskCtor = dataChangeTaskClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, ConfigGroupEnum.class, String.class); + taskCtor.setAccessible(true); + Object dataChangeTask = taskCtor.newInstance(listener, ConfigGroupEnum.PLUGIN, namespaceId); + + Method runMethod = dataChangeTaskClass.getMethod("run"); + runMethod.invoke(dataChangeTask); + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test LongPollingClient run and sendResponse. + */ + @Test + public void testLongPollingClient() throws Exception { + populateAllGroupsInCache(DEFAULT_NAMESPACE, "sameMd5"); + + AsyncContext asyncContext = Mockito.mock(AsyncContext.class); + MockHttpServletRequest req = new MockHttpServletRequest(); + setAllGroupParamsDefault(req); + Mockito.when(asyncContext.getRequest()).thenReturn(req); + Mockito.when(asyncContext.getResponse()).thenReturn(this.httpServletResponse); + + Class longPollingClientClass = Class.forName( + "org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$LongPollingClient"); + java.lang.reflect.Constructor constructor = longPollingClientClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, AsyncContext.class, String.class, long.class, String.class); + constructor.setAccessible(true); + Object longPollingClient = constructor.newInstance(listener, asyncContext, "127.0.0.1", 30000L, "testNamespace"); + + Method runMethod = longPollingClientClass.getMethod("run"); + runMethod.invoke(longPollingClient); + + Method sendResponseMethod = longPollingClientClass.getDeclaredMethod("sendResponse", List.class); + sendResponseMethod.setAccessible(true); + sendResponseMethod.invoke(longPollingClient, List.of(ConfigGroupEnum.PLUGIN)); + + Mockito.verify(asyncContext).complete(); + clearAllGroupsInCache(DEFAULT_NAMESPACE); + } + + /** + * test LongPollingClient sendResponse with null asyncTimeoutFuture (run not called). + */ + @Test + public void testLongPollingClientSendResponseNullFuture() throws Exception { + AsyncContext asyncContext = Mockito.mock(AsyncContext.class); + Mockito.when(asyncContext.getResponse()).thenReturn(this.httpServletResponse); + + Class longPollingClientClass = Class.forName( + "org.apache.shenyu.admin.listener.http.HttpLongPollingDataChangedListener$LongPollingClient"); + java.lang.reflect.Constructor constructor = longPollingClientClass.getDeclaredConstructor( + HttpLongPollingDataChangedListener.class, AsyncContext.class, String.class, long.class, String.class); + constructor.setAccessible(true); + Object longPollingClient = constructor.newInstance(listener, asyncContext, "127.0.0.1", 30000L, "testNamespace"); + + // asyncTimeoutFuture is null (run() not called), sendResponse should still work + Method sendResponseMethod = longPollingClientClass.getDeclaredMethod("sendResponse", List.class); + sendResponseMethod.setAccessible(true); + sendResponseMethod.invoke(longPollingClient, List.of(ConfigGroupEnum.PLUGIN)); + + Mockito.verify(asyncContext).complete(); + } + + /** + * test afterInitialize - verifies scheduler is set up without error. + */ + @Test + public void testAfterInitialize() throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod("afterInitialize"); + method.setAccessible(true); + method.invoke(listener); + } + + // ---- helper methods ---- + + private void populateAllGroupsInCache(final String namespaceId, final String md5) throws Exception { + for (ConfigGroupEnum group : ConfigGroupEnum.values()) { + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group.name()); + getCache().put(cacheKey, new ConfigDataCache(group.name(), "{}", md5, 1000L, namespaceId)); + } + } + + private void clearAllGroupsInCache(final String namespaceId) throws Exception { + for (ConfigGroupEnum group : ConfigGroupEnum.values()) { + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group.name()); + getCache().remove(cacheKey); + } + } + + private void setAllGroupParamsDefault(final MockHttpServletRequest request) { + setAllGroupParamsForNamespace(request, DEFAULT_NAMESPACE); + } + + private void setAllGroupParamsForNamespace(final MockHttpServletRequest request, final String namespaceId) { + for (ConfigGroupEnum group : ConfigGroupEnum.values()) { + String cacheKey = HttpLongPollingDataChangedListener.buildCacheKey(namespaceId, group.name()); + ConfigDataCache existing = getUncheckedCache().get(cacheKey); + String md5 = Objects.nonNull(existing) ? existing.getMd5() : "defaultMd5"; + request.setParameter(group.name(), md5 + ",500"); + } + } + + private ConcurrentMap getUncheckedCache() { + try { + return getCache(); + } catch (Exception e) { + throw new RuntimeException(e); + } + } + + private ConcurrentMap getCache() throws Exception { + Field cacheField = AbstractDataChangedListener.class.getDeclaredField("CACHE"); + cacheField.setAccessible(true); + return (ConcurrentMap) cacheField.get(null); + } + + private boolean invokeCheckCacheDelayAndUpdate(final ConfigDataCache serverCache, + final String clientMd5, final long clientModifyTime) throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "checkCacheDelayAndUpdate", ConfigDataCache.class, String.class, long.class); + method.setAccessible(true); + return (boolean) method.invoke(listener, serverCache, clientMd5, clientModifyTime); + } + + @SuppressWarnings("unchecked") + private List invokeCompareChangedGroup(final HttpServletRequest request) throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "compareChangedGroup", HttpServletRequest.class); + method.setAccessible(true); + return (List) method.invoke(listener, request); + } + + private void invokeGenerateResponse(final jakarta.servlet.http.HttpServletResponse response, + final List changedGroups) throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod( + "generateResponse", jakarta.servlet.http.HttpServletResponse.class, List.class); + method.setAccessible(true); + method.invoke(listener, response, changedGroups); + } + + private String invokeGetRemoteIp(final HttpServletRequest request) throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod("getRemoteIp", HttpServletRequest.class); + method.setAccessible(true); + return (String) method.invoke(null, request); + } + + private String invokeGetNamespaceId(final HttpServletRequest request) throws Exception { + Method method = HttpLongPollingDataChangedListener.class.getDeclaredMethod("getNamespaceId", HttpServletRequest.class); + method.setAccessible(true); + return (String) method.invoke(null, request); + } +} \ No newline at end of file diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java index 4bfc6dc2fb5f..c009e610763e 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketCollectorTest.java @@ -19,11 +19,16 @@ import jakarta.websocket.RemoteEndpoint; import jakarta.websocket.Session; +import org.apache.shenyu.admin.config.properties.ClusterProperties; +import org.apache.shenyu.admin.mode.cluster.service.ClusterSelectMasterService; import org.apache.shenyu.admin.service.SyncDataService; +import org.apache.shenyu.admin.service.publish.InstanceInfoReportEventPublisher; import org.apache.shenyu.admin.spring.SpringBeanUtils; import org.apache.shenyu.admin.utils.ThreadLocalUtils; import org.apache.shenyu.common.constant.Constants; +import org.apache.shenyu.common.constant.InstanceTypeConstants; import org.apache.shenyu.common.enums.DataEventTypeEnum; +import org.apache.shenyu.common.exception.ShenyuException; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -47,11 +52,13 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.isA; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -90,17 +97,35 @@ public static void close() { } @BeforeEach - public void setUp() { + void setUp() { websocketCollector = new WebsocketCollector(); + // Clear shared static state between tests + clearStaticSessionState(); when(session.isOpen()).thenReturn(true); Map userProperties = new HashMap<>(); userProperties.put(Constants.SHENYU_NAMESPACE_ID, Constants.SYS_DEFAULT_NAMESPACE_ID); when(session.getUserProperties()).thenReturn(userProperties); + + ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); + SpringBeanUtils.getInstance().setApplicationContext(context); + ThreadLocalUtils.remove("sessionKey"); } - @Test - public void testOnOpen() { + @SuppressWarnings("unchecked") + private void clearStaticSessionState() { + Set sessionSet = (Set) ReflectionTestUtils.getField(WebsocketCollector.class, "SESSION_SET"); + if (Objects.nonNull(sessionSet)) { + sessionSet.clear(); + } + Map> namespaceMap = + (Map>) ReflectionTestUtils.getField(WebsocketCollector.class, "NAMESPACE_SESSION_MAP"); + if (Objects.nonNull(namespaceMap)) { + namespaceMap.clear(); + } + } + @Test + void testOnOpen() { websocketCollector.onOpen(session); assertEquals(1L, getSessionSetSize()); doNothing().when(loggerSpy).warn(anyString(), anyString()); @@ -108,9 +133,29 @@ public void testOnOpen() { } @Test - public void testOnMessage() { - ConfigurableApplicationContext context = mock(ConfigurableApplicationContext.class); - SpringBeanUtils.getInstance().setApplicationContext(context); + void testOnOpenWithBlankNamespaceIdThrows() { + Map userProperties = new HashMap<>(); + // no SHENYU_NAMESPACE_ID set → getNamespaceId returns null → throws ShenyuException + when(session.getUserProperties()).thenReturn(userProperties); + assertThrows(ShenyuException.class, () -> websocketCollector.onOpen(session)); + // clean up the session that was added before throw + websocketCollector.onClose(session); + } + + @Test + void testOnOpenWithClientIp() { + Map userProperties = new HashMap<>(); + userProperties.put(Constants.SHENYU_NAMESPACE_ID, Constants.SYS_DEFAULT_NAMESPACE_ID); + userProperties.put(WebsocketListener.CLIENT_IP_NAME, "192.168.1.1"); + when(session.getUserProperties()).thenReturn(userProperties); + + websocketCollector.onOpen(session); + assertEquals(1L, getSessionSetSize()); + websocketCollector.onClose(session); + } + + @Test + void testOnMessage() { when(SpringBeanUtils.getInstance().getBean(SyncDataService.class)).thenReturn(syncDataService); when(syncDataService.syncAllByNamespaceId(DataEventTypeEnum.MYSELF, Constants.SYS_DEFAULT_NAMESPACE_ID)).thenReturn(true); websocketCollector.onOpen(session); @@ -122,7 +167,72 @@ public void testOnMessage() { } @Test - public void testOnClose() { + void testOnMessageUnknownMessageReturnsEarly() { + websocketCollector.onOpen(session); + // Unknown message — early return, no service calls + websocketCollector.onMessage("UNKNOWN_EVENT", session); + verify(syncDataService, never()).syncAllByNamespaceId(DataEventTypeEnum.MYSELF, Constants.SYS_DEFAULT_NAMESPACE_ID); + websocketCollector.onClose(session); + } + + @Test + void testOnMessageRunningModeStandalone() throws IOException { + ClusterProperties clusterProperties = mock(ClusterProperties.class); + when(clusterProperties.isEnabled()).thenReturn(false); + when(SpringBeanUtils.getInstance().getBean(ClusterProperties.class)).thenReturn(clusterProperties); + + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + + websocketCollector.onOpen(session); + websocketCollector.onMessage(DataEventTypeEnum.RUNNING_MODE.name(), session); + + verify(basic, times(1)).sendText(anyString()); + websocketCollector.onClose(session); + ThreadLocalUtils.remove("sessionKey"); + } + + @Test + void testOnMessageRunningModeCluster() throws IOException { + ClusterProperties clusterProperties = mock(ClusterProperties.class); + when(clusterProperties.isEnabled()).thenReturn(true); + ClusterSelectMasterService masterService = mock(ClusterSelectMasterService.class); + when(masterService.isMaster()).thenReturn(true); + when(masterService.getMasterUrl()).thenReturn("http://localhost:9095"); + when(SpringBeanUtils.getInstance().getBean(ClusterProperties.class)).thenReturn(clusterProperties); + when(SpringBeanUtils.getInstance().getBean(ClusterSelectMasterService.class)).thenReturn(masterService); + + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + + websocketCollector.onOpen(session); + websocketCollector.onMessage(DataEventTypeEnum.RUNNING_MODE.name(), session); + + verify(basic, times(1)).sendText(anyString()); + websocketCollector.onClose(session); + ThreadLocalUtils.remove("sessionKey"); + } + + @Test + void testOnMessageBootstrapInstanceInfo() { + InstanceInfoReportEventPublisher publisher = mock(InstanceInfoReportEventPublisher.class); + when(SpringBeanUtils.getInstance().getBean(InstanceInfoReportEventPublisher.class)).thenReturn(publisher); + + Map userProperties = new HashMap<>(); + userProperties.put(Constants.SHENYU_NAMESPACE_ID, Constants.SYS_DEFAULT_NAMESPACE_ID); + userProperties.put(Constants.CLIENT_PORT_NAME, "8080"); + when(session.getUserProperties()).thenReturn(userProperties); + + websocketCollector.onOpen(session); + String bootstrapMsg = "{\"" + InstanceTypeConstants.BOOTSTRAP_INSTANCE_INFO + "\":{\"key\":\"val\"}}"; + websocketCollector.onMessage(bootstrapMsg, session); + + verify(publisher, times(1)).publish(isA(org.apache.shenyu.admin.model.event.instance.InstanceInfoReportEvent.class)); + websocketCollector.onClose(session); + } + + @Test + void testOnClose() { websocketCollector.onOpen(session); assertEquals(1L, getSessionSetSize()); doNothing().when(loggerSpy).warn(anyString(), anyString()); @@ -132,7 +242,25 @@ public void testOnClose() { } @Test - public void testOnError() { + void testOnCloseWithBlankNamespaceId() { + // Session with no namespace — clearSession should still work (blank namespaceId branch) + Map props = new HashMap<>(); + props.put(Constants.SHENYU_NAMESPACE_ID, Constants.SYS_DEFAULT_NAMESPACE_ID); + when(session.getUserProperties()).thenReturn(props); + websocketCollector.onOpen(session); + + // Now change namespace to blank before close + Session session2 = mock(Session.class); + when(session2.isOpen()).thenReturn(false); + when(session2.getUserProperties()).thenReturn(new HashMap<>()); + websocketCollector.onClose(session2); + // original session still tracked + assertEquals(1L, getSessionSetSize()); + websocketCollector.onClose(session); + } + + @Test + void testOnError() { websocketCollector.onOpen(session); assertEquals(1L, getSessionSetSize()); doNothing().when(loggerSpy).error(anyString(), anyString(), isA(Throwable.class)); @@ -143,7 +271,7 @@ public void testOnError() { } @Test - public void testSend() throws IOException { + void testSendOldApi() throws IOException { RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); when(session.getBasicRemote()).thenReturn(basic); when(session.isOpen()).thenReturn(true); @@ -161,6 +289,110 @@ public void testSend() throws IOException { ThreadLocalUtils.remove("sessionKey"); } + @Test + void testSendOldApiMyselfClosedSession() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + websocketCollector.onOpen(session); + + // Mark session as closed + when(session.isOpen()).thenReturn(false); + ThreadLocalUtils.put("sessionKey", session); + WebsocketCollector.send("msg", DataEventTypeEnum.MYSELF); + // closed session → removed from SESSION_SET, no sendText + verify(basic, never()).sendText("msg"); + assertEquals(0L, getSessionSetSize()); + ThreadLocalUtils.remove("sessionKey"); + } + + @Test + void testSendOldApiMyselfNullSession() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + // No session in ThreadLocal + ThreadLocalUtils.remove("sessionKey"); + WebsocketCollector.send("msg", DataEventTypeEnum.MYSELF); + verify(basic, never()).sendText(anyString()); + } + + @Test + void testSendWithNamespaceIdBlankMessageNoOp() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + WebsocketCollector.send(Constants.SYS_DEFAULT_NAMESPACE_ID, "", DataEventTypeEnum.CREATE); + verify(basic, never()).sendText(anyString()); + } + + @Test + void testSendWithNamespaceIdBlankNamespaceThrows() { + assertThrows(ShenyuException.class, + () -> WebsocketCollector.send("", "some-message", DataEventTypeEnum.CREATE)); + } + + @Test + void testSendWithNamespaceIdMyself() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + when(session.isOpen()).thenReturn(true); + websocketCollector.onOpen(session); + + ThreadLocalUtils.put("sessionKey", session); + WebsocketCollector.send(Constants.SYS_DEFAULT_NAMESPACE_ID, "ns-msg", DataEventTypeEnum.MYSELF); + verify(basic, times(1)).sendText("ns-msg"); + + websocketCollector.onClose(session); + ThreadLocalUtils.remove("sessionKey"); + } + + @Test + void testSendWithNamespaceIdMyselfClosedSession() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + websocketCollector.onOpen(session); + + when(session.isOpen()).thenReturn(false); + ThreadLocalUtils.put("sessionKey", session); + WebsocketCollector.send(Constants.SYS_DEFAULT_NAMESPACE_ID, "ns-msg", DataEventTypeEnum.MYSELF); + verify(basic, never()).sendText("ns-msg"); + + ThreadLocalUtils.remove("sessionKey"); + } + + @Test + void testSendWithNamespaceIdMyselfNullSession() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + ThreadLocalUtils.remove("sessionKey"); + WebsocketCollector.send(Constants.SYS_DEFAULT_NAMESPACE_ID, "ns-msg", DataEventTypeEnum.MYSELF); + verify(basic, never()).sendText(anyString()); + } + + @Test + void testSendWithNamespaceIdNonMyselfBroadcast() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + when(session.isOpen()).thenReturn(true); + websocketCollector.onOpen(session); + + WebsocketCollector.send(Constants.SYS_DEFAULT_NAMESPACE_ID, "broadcast-msg", DataEventTypeEnum.CREATE); + verify(basic, times(1)).sendText("broadcast-msg"); + + websocketCollector.onClose(session); + } + + @Test + void testSendBySessionIOException() throws IOException { + RemoteEndpoint.Basic basic = mock(RemoteEndpoint.Basic.class); + when(session.getBasicRemote()).thenReturn(basic); + when(session.isOpen()).thenReturn(true); + websocketCollector.onOpen(session); + + // IOException on sendText should be caught internally + org.mockito.Mockito.doThrow(new IOException("io error")).when(basic).sendText(anyString()); + WebsocketCollector.send(Constants.SYS_DEFAULT_NAMESPACE_ID, "fail-msg", DataEventTypeEnum.CREATE); + // no exception propagated; verify attempted send + verify(basic, times(1)).sendText("fail-msg"); + + websocketCollector.onClose(session); + } + private long getSessionSetSize() { Set sessionSet = (Set) ReflectionTestUtils.getField(WebsocketCollector.class, "SESSION_SET"); return Objects.isNull(sessionSet) ? -1 : sessionSet.size(); diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketConfiguratorTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketConfiguratorTest.java index fa161a7ef954..c802651e61c9 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketConfiguratorTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketConfiguratorTest.java @@ -17,15 +17,27 @@ package org.apache.shenyu.admin.listener.websocket; -import org.junit.jupiter.api.Test; - +import jakarta.servlet.ServletContext; import jakarta.servlet.http.HttpSession; import jakarta.websocket.HandshakeResponse; import jakarta.websocket.server.HandshakeRequest; import jakarta.websocket.server.ServerEndpointConfig; +import org.apache.shenyu.admin.config.properties.WebsocketSyncProperties; +import org.apache.shenyu.admin.spring.SpringBeanUtils; +import org.apache.shenyu.common.constant.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.context.ApplicationContext; +import org.springframework.test.util.ReflectionTestUtils; +import java.util.HashMap; import java.util.Map; +import static org.apache.tomcat.websocket.server.Constants.BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM; +import static org.apache.tomcat.websocket.server.Constants.TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -35,17 +47,125 @@ */ public class WebsocketConfiguratorTest { + private WebsocketConfigurator websocketConfigurator; + + private WebsocketSyncProperties websocketSyncProperties; + + @BeforeEach + void setUp() { + websocketConfigurator = new WebsocketConfigurator(); + websocketSyncProperties = new WebsocketSyncProperties(); + ReflectionTestUtils.setField(websocketConfigurator, "websocketSyncProperties", websocketSyncProperties); + + ApplicationContext applicationContext = mock(ApplicationContext.class); + when(applicationContext.getBean(WebsocketSyncProperties.class)).thenReturn(websocketSyncProperties); + SpringBeanUtils.getInstance().setApplicationContext(applicationContext); + } + @Test - public void testModifyHandshake() { - WebsocketConfigurator websocketConfigurator = new WebsocketConfigurator(); + void testModifyHandshake() { ServerEndpointConfig sec = mock(ServerEndpointConfig.class); - Map userProperties = mock(Map.class); + Map userProperties = new HashMap<>(); when(sec.getUserProperties()).thenReturn(userProperties); + HandshakeRequest request = mock(HandshakeRequest.class); HttpSession httpSession = mock(HttpSession.class); when(request.getHttpSession()).thenReturn(httpSession); + when(httpSession.getAttribute(WebsocketListener.CLIENT_IP_NAME)).thenReturn("192.168.1.1"); + when(httpSession.getAttribute(Constants.CLIENT_PORT_NAME)).thenReturn("8080"); + when(httpSession.getAttribute(Constants.SHENYU_NAMESPACE_ID)).thenReturn("ns-123"); + HandshakeResponse response = mock(HandshakeResponse.class); websocketConfigurator.modifyHandshake(sec, request, response); - verify(userProperties).put(WebsocketListener.CLIENT_IP_NAME, httpSession.getAttribute(WebsocketListener.CLIENT_IP_NAME)); + + assertEquals("192.168.1.1", userProperties.get(WebsocketListener.CLIENT_IP_NAME)); + assertEquals("8080", userProperties.get(Constants.CLIENT_PORT_NAME)); + assertEquals("ns-123", userProperties.get(Constants.SHENYU_NAMESPACE_ID)); + } + + @Test + void testModifyHandshakePutsAllAttributes() { + ServerEndpointConfig sec = mock(ServerEndpointConfig.class); + Map userProperties = new HashMap<>(); + when(sec.getUserProperties()).thenReturn(userProperties); + + HandshakeRequest request = mock(HandshakeRequest.class); + HttpSession httpSession = mock(HttpSession.class); + when(request.getHttpSession()).thenReturn(httpSession); + + HandshakeResponse response = mock(HandshakeResponse.class); + websocketConfigurator.modifyHandshake(sec, request, response); + + verify(httpSession).getAttribute(WebsocketListener.CLIENT_IP_NAME); + verify(httpSession).getAttribute(Constants.CLIENT_PORT_NAME); + verify(httpSession).getAttribute(Constants.SHENYU_NAMESPACE_ID); + assertTrue(userProperties.containsKey(WebsocketListener.CLIENT_IP_NAME)); + assertTrue(userProperties.containsKey(Constants.CLIENT_PORT_NAME)); + assertTrue(userProperties.containsKey(Constants.SHENYU_NAMESPACE_ID)); + } + + @Test + void testCheckOriginAllowedWhenAllowOriginsEmpty() { + websocketSyncProperties.setAllowOrigins(""); + // empty allowOrigins → delegates to super (always true for any origin) + assertTrue(websocketConfigurator.checkOrigin("http://any-origin.com")); + } + + @Test + void testCheckOriginAllowedWhenAllowOriginsNull() { + websocketSyncProperties.setAllowOrigins(null); + assertTrue(websocketConfigurator.checkOrigin("http://any-origin.com")); + } + + @Test + void testCheckOriginAllowedWhenOriginMatches() { + websocketSyncProperties.setAllowOrigins("http://allowed.com;http://other.com"); + assertTrue(websocketConfigurator.checkOrigin("http://allowed.com")); + } + + @Test + void testCheckOriginAllowedWhenSecondOriginMatches() { + websocketSyncProperties.setAllowOrigins("http://first.com;http://second.com"); + assertTrue(websocketConfigurator.checkOrigin("http://second.com")); + } + + @Test + void testCheckOriginForbiddenWhenNoMatch() { + websocketSyncProperties.setAllowOrigins("http://allowed.com;http://other.com"); + assertFalse(websocketConfigurator.checkOrigin("http://forbidden.com")); + } + + @Test + void testOnStartupWithPositiveMessageMaxSize() { + websocketSyncProperties.setMessageMaxSize(65536); + ServletContext servletContext = mock(ServletContext.class); + + websocketConfigurator.onStartup(servletContext); + + verify(servletContext).setInitParameter(TEXT_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM, "65536"); + verify(servletContext).setInitParameter(BINARY_BUFFER_SIZE_SERVLET_CONTEXT_INIT_PARAM, "65536"); + } + + @Test + void testOnStartupWithZeroMessageMaxSize() { + websocketSyncProperties.setMessageMaxSize(0); + ServletContext servletContext = mock(ServletContext.class); + + websocketConfigurator.onStartup(servletContext); + + // messageMaxSize <= 0 → no setInitParameter calls + verify(servletContext, org.mockito.Mockito.never()) + .setInitParameter(org.mockito.Mockito.anyString(), org.mockito.Mockito.anyString()); + } + + @Test + void testOnStartupWithNegativeMessageMaxSize() { + websocketSyncProperties.setMessageMaxSize(-1); + ServletContext servletContext = mock(ServletContext.class); + + websocketConfigurator.onStartup(servletContext); + + verify(servletContext, org.mockito.Mockito.never()) + .setInitParameter(org.mockito.Mockito.anyString(), org.mockito.Mockito.anyString()); } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataChangedListenerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataChangedListenerTest.java index e439d9d6243a..4c7ee9f5002a 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataChangedListenerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketDataChangedListenerTest.java @@ -17,33 +17,38 @@ package org.apache.shenyu.admin.listener.websocket; +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.shenyu.common.constant.Constants; -import org.apache.shenyu.common.dto.MetaData; -import org.apache.shenyu.common.dto.AuthPathData; -import org.apache.shenyu.common.dto.AuthParamData; import org.apache.shenyu.common.dto.AppAuthData; -import org.apache.shenyu.common.dto.RuleData; -import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.AuthParamData; +import org.apache.shenyu.common.dto.AuthPathData; import org.apache.shenyu.common.dto.ConditionData; +import org.apache.shenyu.common.dto.DiscoverySyncData; +import org.apache.shenyu.common.dto.MetaData; +import org.apache.shenyu.common.dto.PluginData; +import org.apache.shenyu.common.dto.ProxyApiKeyData; +import org.apache.shenyu.common.dto.ProxySelectorData; +import org.apache.shenyu.common.dto.RuleData; import org.apache.shenyu.common.dto.SelectorData; import org.apache.shenyu.common.enums.DataEventTypeEnum; -import org.junit.jupiter.api.Test; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.MockedStatic; import org.mockito.junit.jupiter.MockitoExtension; import org.mockito.stubbing.Answer; import java.util.ArrayList; +import java.util.Collections; import java.util.List; -import static org.mockito.Mockito.mockStatic; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.ArgumentMatchers.eq; -import com.fasterxml.jackson.databind.JsonNode; -import com.fasterxml.jackson.databind.ObjectMapper; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; /** * Data Change WebSocketListener Test. @@ -86,96 +91,79 @@ public void testOnPluginChanged() { websocketDataChangedListener.onPluginChanged(pluginDataList, DataEventTypeEnum.UPDATE); mockedStatic.verify(() -> WebsocketCollector.send( eq(Constants.SYS_DEFAULT_NAMESPACE_ID), - argThat(actualMsg -> { - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode expectedJson = mapper.readTree(message); - JsonNode actualJson = mapper.readTree(actualMsg); - return expectedJson.equals(actualJson); - } catch (Exception e) { - return false; - } - }), + argThat(actualMsg -> jsonEquals(message, actualMsg)), eq(DataEventTypeEnum.UPDATE) )); } } + /** + * test PluginData with empty list — no send. + */ + @Test + public void testOnPluginChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onPluginChanged(Collections.emptyList(), DataEventTypeEnum.UPDATE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + /** * test SelectorData. */ @Test public void testOnSelectorChanged() { - String message = "{\"groupType\":\"SELECTOR\",\"eventType\":\"UPDATE\",\"data\":" - + "[{\"pluginId\":\"5\",\"pluginName\":\"divide\"," - + "\"matchMode\":0,\"type\":1,\"logged\":true," - + "\"continued\":true,\"handle\":\"[{\\\\\\\"upstreamHost\\\\\\\":\\\\\\\"localhost\\\\\\\"," - + "\\\\\\\"protocol\\\\\\\":\\\\\\\"http://\\\\\\\",\\\\\\\"upstreamUrl\\\\\\\":" - + "\\\\\\\"127.0.0.1:8187\\\\\\\",\\\\\\\"weight\\\\\\\":\\\\\\\"51\\\\\\\"}," - + "{\\\\\\\"upstreamHost\\\\\\\":\\\\\\\"localhost\\\\\\\",\\\\\\\"protocol\\\\\\\":" - + "\\\\\\\"http://\\\\\\\",\\\\\\\"upstreamUrl\\\\\\\":\\\\\\\"127.0.0.1:8188\\\\\\\"," - + "\\\\\\\"weight\\\\\\\":\\\\\\\"49\\\\\\\"}]\",\"conditionList\":[{\"paramType\":\"uri\"," - + "\"operator\":\"match\",\"paramName\":\"/\",\"paramValue\":\"/http/**\"}],\"id\":\"1336329408516136960\"," - + "\"name\":\"/http\",\"enabled\":true,\"sort\":1,\"namespaceId\":\"649330b6-c2d7-4edc-be8e-8a54df9eb385\"}]}"; - try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { mockedStatic.when(() -> WebsocketCollector.send(anyString(), anyString(), any())) .thenAnswer(invocation -> null); - - // 调用被测试的方法 websocketDataChangedListener.onSelectorChanged(selectorDataList, DataEventTypeEnum.UPDATE); - - // 验证 mockedStatic.verify(() -> WebsocketCollector.send( eq(Constants.SYS_DEFAULT_NAMESPACE_ID), - argThat(actualMsg -> { - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode expectedJson = mapper.readTree(message); - JsonNode actualJson = mapper.readTree(actualMsg); - return expectedJson.equals(actualJson); - } catch (Exception e) { - return false; - } - }), + anyString(), eq(DataEventTypeEnum.UPDATE) )); } } + /** + * test SelectorData with empty list — no send. + */ + @Test + public void testOnSelectorChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onSelectorChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + /** * test RuleData. */ @Test public void testOnRuleChanged() { - String message = "{\"groupType\":\"RULE\",\"eventType\":\"UPDATE\",\"data\":[{" - + "\"pluginName\":\"waf\",\"selectorId\":\"1336349806465064960\"," - + "\"matchMode\":1,\"loged\":true,\"handle\":" - + "\"{\\\\\\\"permission\\\\\\\":\\\\\\\"reject\\\\\\\",\\\\\\\"statusCode\\\\\\\":" - + "\\\\\\\"503\\\\\\\"}\",\"conditionDataList\":[{\"paramType\":\"header\",\"operator\":" - + "\"\\u003d\",\"paramName\":\"test\",\"paramValue\":\"a\"}],\"id\":\"1336350040008105984\",\"name\":\"test\"," - + "\"enabled\":true,\"sort\":1,\"namespaceId\":\"649330b6-c2d7-4edc-be8e-8a54df9eb385\"}]}"; try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { mockedStatic.when(() -> WebsocketCollector.send(anyString(), anyString(), any())) .thenAnswer(invocation -> null); websocketDataChangedListener.onRuleChanged(ruleDataList, DataEventTypeEnum.UPDATE); mockedStatic.verify(() -> WebsocketCollector.send( eq(Constants.SYS_DEFAULT_NAMESPACE_ID), - argThat(actualMsg -> { - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode expectedJson = mapper.readTree(message); - JsonNode actualJson = mapper.readTree(actualMsg); - return expectedJson.equals(actualJson); - } catch (Exception e) { - return false; - } - }), + anyString(), eq(DataEventTypeEnum.UPDATE) )); } } + /** + * test RuleData with empty list — no send. + */ + @Test + public void testOnRuleChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onRuleChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + /** * test AppAuthData. */ @@ -191,21 +179,23 @@ public void testOnAppAuthChanged() { websocketDataChangedListener.onAppAuthChanged(appAuthDataList, DataEventTypeEnum.UPDATE); mockedStatic.verify(() -> WebsocketCollector.send( eq(Constants.SYS_DEFAULT_NAMESPACE_ID), - argThat(actualMsg -> { - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode expectedJson = mapper.readTree(message); - JsonNode actualJson = mapper.readTree(actualMsg); - return expectedJson.equals(actualJson); - } catch (Exception e) { - return false; - } - }), + argThat(actualMsg -> jsonEquals(message, actualMsg)), eq(DataEventTypeEnum.UPDATE) )); } } + /** + * test AppAuthData with empty list — no send. + */ + @Test + public void testOnAppAuthChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onAppAuthChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + /** * test MetaData. */ @@ -220,21 +210,162 @@ public void testOnMetaDataChanged() { websocketDataChangedListener.onMetaDataChanged(metaDataList, DataEventTypeEnum.CREATE); mockedStatic.verify(() -> WebsocketCollector.send( eq(Constants.SYS_DEFAULT_NAMESPACE_ID), - argThat(actualMsg -> { - try { - ObjectMapper mapper = new ObjectMapper(); - JsonNode expectedJson = mapper.readTree(message); - JsonNode actualJson = mapper.readTree(actualMsg); - return expectedJson.equals(actualJson); - } catch (Exception e) { - return false; - } - }), + argThat(actualMsg -> jsonEquals(message, actualMsg)), eq(DataEventTypeEnum.CREATE) )); } } + /** + * test MetaData with empty list — no send. + */ + @Test + public void testOnMetaDataChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onMetaDataChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + + /** + * test ProxySelectorData. + */ + @Test + public void testOnProxySelectorChanged() { + ProxySelectorData data = new ProxySelectorData(); + data.setId("ps-1"); + data.setName("proxySelector"); + data.setPluginName("tcp"); + data.setNamespaceId(Constants.SYS_DEFAULT_NAMESPACE_ID); + List list = Collections.singletonList(data); + + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + mockedStatic.when(() -> WebsocketCollector.send(anyString(), anyString(), any())) + .thenAnswer(invocation -> null); + websocketDataChangedListener.onProxySelectorChanged(list, DataEventTypeEnum.UPDATE); + mockedStatic.verify(() -> WebsocketCollector.send( + eq(Constants.SYS_DEFAULT_NAMESPACE_ID), + anyString(), + eq(DataEventTypeEnum.UPDATE) + )); + } + } + + /** + * test ProxySelectorData with empty list — no send. + */ + @Test + public void testOnProxySelectorChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onProxySelectorChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + + /** + * test ProxyApiKeyData. + */ + @Test + public void testOnAiProxyApiKeyChanged() { + ProxyApiKeyData data = ProxyApiKeyData.builder() + .realApiKey("real-key") + .proxyApiKey("proxy-key") + .namespaceId(Constants.SYS_DEFAULT_NAMESPACE_ID) + .build(); + List list = Collections.singletonList(data); + + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + mockedStatic.when(() -> WebsocketCollector.send(anyString(), anyString(), any())) + .thenAnswer(invocation -> null); + websocketDataChangedListener.onAiProxyApiKeyChanged(list, DataEventTypeEnum.CREATE); + mockedStatic.verify(() -> WebsocketCollector.send( + eq(Constants.SYS_DEFAULT_NAMESPACE_ID), + anyString(), + eq(DataEventTypeEnum.CREATE) + )); + } + } + + /** + * test ProxyApiKeyData with empty list — no send. + */ + @Test + public void testOnAiProxyApiKeyChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onAiProxyApiKeyChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + + /** + * test DiscoverySyncData. + */ + @Test + public void testOnDiscoveryUpstreamChanged() { + DiscoverySyncData data = new DiscoverySyncData(); + data.setSelectorId("sel-1"); + data.setPluginName("divide"); + data.setSelectorName("test"); + data.setNamespaceId(Constants.SYS_DEFAULT_NAMESPACE_ID); + List list = Collections.singletonList(data); + + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + mockedStatic.when(() -> WebsocketCollector.send(anyString(), anyString(), any())) + .thenAnswer(invocation -> null); + websocketDataChangedListener.onDiscoveryUpstreamChanged(list, DataEventTypeEnum.UPDATE); + mockedStatic.verify(() -> WebsocketCollector.send( + eq(Constants.SYS_DEFAULT_NAMESPACE_ID), + anyString(), + eq(DataEventTypeEnum.UPDATE) + )); + } + } + + /** + * test DiscoverySyncData with empty list — no send. + */ + @Test + public void testOnDiscoveryUpstreamChangedEmptyList() { + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + websocketDataChangedListener.onDiscoveryUpstreamChanged(Collections.emptyList(), DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send(anyString(), anyString(), any()), never()); + } + } + + /** + * test namespaceId fallback to default when null. + */ + @Test + public void testOnPluginChangedNullNamespaceUsesDefault() { + PluginData data = new PluginData(); + data.setId("3"); + data.setName("test-plugin"); + data.setNamespaceId(null); + List list = Collections.singletonList(data); + + try (MockedStatic mockedStatic = mockStatic(WebsocketCollector.class)) { + mockedStatic.when(() -> WebsocketCollector.send(anyString(), anyString(), any())) + .thenAnswer(invocation -> null); + websocketDataChangedListener.onPluginChanged(list, DataEventTypeEnum.DELETE); + mockedStatic.verify(() -> WebsocketCollector.send( + eq(Constants.SYS_DEFAULT_NAMESPACE_ID), + anyString(), + eq(DataEventTypeEnum.DELETE) + )); + } + } + + private boolean jsonEquals(final String expected, final String actual) { + try { + ObjectMapper mapper = new ObjectMapper(); + JsonNode expectedJson = mapper.readTree(expected); + JsonNode actualJson = mapper.readTree(actual); + return expectedJson.equals(actualJson); + } catch (Exception e) { + return false; + } + } + private void initMetaDataList() { MetaData metaData = new MetaData(); metaData.setAppName("axiba"); @@ -322,7 +453,6 @@ private List buildAuthParamDataList(final String appName, final S AuthParamData authParamData = new AuthParamData(); authParamData.setAppName(appName); authParamData.setAppParam(appParam); - List authParamDataList = new ArrayList<>(); authParamDataList.add(authParamData); return authParamDataList; @@ -333,7 +463,6 @@ private List buildAuthPathDataList(final String appName, final Str authPathData.setAppName(appName); authPathData.setEnabled(true); authPathData.setPath(path); - List authPathDataList = new ArrayList<>(); authPathDataList.add(authPathData); return authPathDataList; diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketListenerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketListenerTest.java index 41a789cc2d3d..ac1a91e68848 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketListenerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/listener/websocket/WebsocketListenerTest.java @@ -17,16 +17,16 @@ package org.apache.shenyu.admin.listener.websocket; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - import jakarta.servlet.ServletRequestEvent; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; +import org.apache.shenyu.common.constant.Constants; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import static org.apache.shenyu.admin.listener.websocket.WebsocketListener.CLIENT_IP_NAME; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.times; +import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -38,32 +38,116 @@ public class WebsocketListenerTest { private WebsocketListener websocketListener; @BeforeEach - public void setUp() { + void setUp() { this.websocketListener = new WebsocketListener(); } @Test - public void testRequestDestroyed() { + void testRequestDestroyed() { ServletRequestEvent sre = mock(ServletRequestEvent.class); HttpServletRequest request = mock(HttpServletRequest.class); HttpSession session = mock(HttpSession.class); when(sre.getServletRequest()).thenReturn(request); when(request.getSession()).thenReturn(session); + websocketListener.requestDestroyed(sre); + verify(request).removeAttribute(CLIENT_IP_NAME); verify(session).removeAttribute(CLIENT_IP_NAME); + verify(request).removeAttribute(Constants.SHENYU_NAMESPACE_ID); + verify(session).removeAttribute(Constants.SHENYU_NAMESPACE_ID); + verify(request).removeAttribute(Constants.CLIENT_PORT_NAME); + verify(session).removeAttribute(Constants.CLIENT_PORT_NAME); + } + + @Test + void testRequestDestroyedWhenSessionIsNull() { + ServletRequestEvent sre = mock(ServletRequestEvent.class); + HttpServletRequest request = mock(HttpServletRequest.class); + when(sre.getServletRequest()).thenReturn(request); + when(request.getSession()).thenReturn(null); + + // Should not throw; inner if-guard skips body + websocketListener.requestDestroyed(sre); + + verify(request, never()).removeAttribute(CLIENT_IP_NAME); + } + + @Test + void testRequestDestroyedWhenExceptionThrown() { + ServletRequestEvent sre = mock(ServletRequestEvent.class); + HttpServletRequest request = mock(HttpServletRequest.class); + when(sre.getServletRequest()).thenReturn(request); + // getSession() throws RuntimeException, caught by try/catch in listener + when(request.getSession()).thenThrow(new RuntimeException("session error")); + + // Should not propagate; exception is caught internally + websocketListener.requestDestroyed(sre); + } + + @Test + void testRequestInitialized() { + ServletRequestEvent sre = mock(ServletRequestEvent.class); + HttpServletRequest request = mock(HttpServletRequest.class); + HttpSession session = mock(HttpSession.class); + when(sre.getServletRequest()).thenReturn(request); + when(request.getSession()).thenReturn(session); + when(request.getRemoteAddr()).thenReturn("10.0.0.1"); + when(request.getHeader(Constants.SHENYU_NAMESPACE_ID)).thenReturn(null); + + websocketListener.requestInitialized(sre); + + // request.setAttribute(CLIENT_IP_NAME, ...) is called once directly on the request + verify(request).setAttribute(CLIENT_IP_NAME, "10.0.0.1"); + verify(session).setAttribute(CLIENT_IP_NAME, "10.0.0.1"); + // namespace is null → no setAttribute for SHENYU_NAMESPACE_ID or CLIENT_PORT_NAME + verify(request, never()).setAttribute(Constants.SHENYU_NAMESPACE_ID, null); + verify(session, never()).setAttribute(Constants.SHENYU_NAMESPACE_ID, null); } @Test - public void testRequestInitialized() { + void testRequestInitializedWithNamespaceHeader() { ServletRequestEvent sre = mock(ServletRequestEvent.class); HttpServletRequest request = mock(HttpServletRequest.class); HttpSession session = mock(HttpSession.class); when(sre.getServletRequest()).thenReturn(request); when(request.getSession()).thenReturn(session); + when(request.getRemoteAddr()).thenReturn("10.0.0.2"); + when(request.getHeader(Constants.SHENYU_NAMESPACE_ID)).thenReturn("ns-abc"); + when(request.getHeader(Constants.CLIENT_PORT_NAME)).thenReturn("9090"); + + websocketListener.requestInitialized(sre); + + verify(request).setAttribute(CLIENT_IP_NAME, "10.0.0.2"); + verify(session).setAttribute(CLIENT_IP_NAME, "10.0.0.2"); + verify(request).setAttribute(Constants.SHENYU_NAMESPACE_ID, "ns-abc"); + verify(session).setAttribute(Constants.SHENYU_NAMESPACE_ID, "ns-abc"); + verify(request).setAttribute(Constants.CLIENT_PORT_NAME, "9090"); + verify(session).setAttribute(Constants.CLIENT_PORT_NAME, "9090"); + } + + @Test + void testRequestInitializedWhenSessionIsNull() { + ServletRequestEvent sre = mock(ServletRequestEvent.class); + HttpServletRequest request = mock(HttpServletRequest.class); + when(sre.getServletRequest()).thenReturn(request); + when(request.getSession()).thenReturn(null); + + // Inner if-guard skips the body; no setAttribute should be called + websocketListener.requestInitialized(sre); + + verify(request, never()).setAttribute(CLIENT_IP_NAME, null); + } + + @Test + void testRequestInitializedWhenExceptionThrown() { + ServletRequestEvent sre = mock(ServletRequestEvent.class); + HttpServletRequest request = mock(HttpServletRequest.class); + when(sre.getServletRequest()).thenReturn(request); + // getSession() throws RuntimeException, caught by try/catch in listener + when(request.getSession()).thenThrow(new RuntimeException("session error")); + + // Should not propagate; exception is caught internally websocketListener.requestInitialized(sre); - // For session will invoke request one more time. - verify(request, times(2)).setAttribute(CLIENT_IP_NAME, sre.getServletRequest().getRemoteAddr()); - verify(session).setAttribute(CLIENT_IP_NAME, sre.getServletRequest().getRemoteAddr()); } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/DivideSelectorHandleConverterTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/DivideSelectorHandleConverterTest.java new file mode 100644 index 000000000000..44a659bf741d --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/DivideSelectorHandleConverterTest.java @@ -0,0 +1,239 @@ +/* + * 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 org.apache.shenyu.admin.service.converter; + +import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; +import org.apache.shenyu.common.dto.convert.selector.DivideUpstream; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link DivideSelectorHandleConverter}. + */ +class DivideSelectorHandleConverterTest { + + private DivideSelectorHandleConverter converter; + + @BeforeEach + void setUp() { + converter = new DivideSelectorHandleConverter(); + } + + @Test + void testPluginName() { + String pluginName = converter.pluginName(); + assertThat(pluginName, is(PluginEnum.DIVIDE.getName())); + assertEquals("divide", pluginName); + } + + @Test + void testConvertUpstream() { + DivideUpstream upstream1 = buildDivideUpstream("http://", "localhost:8080", true, 100, 10); + DivideUpstream upstream2 = buildDivideUpstream("http://", "localhost:8081", true, 50, 5); + List upstreamList = new ArrayList<>(); + upstreamList.add(upstream1); + upstreamList.add(upstream2); + + String handle = GsonUtils.getInstance().toJson(upstreamList); + List result = converter.convertUpstream(handle); + + assertThat(result, notNullValue()); + assertThat(result, hasSize(2)); + assertEquals("http://", result.get(0).getProtocol()); + assertEquals("localhost:8080", result.get(0).getUpstreamUrl()); + assertTrue(result.get(0).isStatus()); + } + + @Test + void testConvertUpstreamWithEmptyHandle() { + List result = converter.convertUpstream("[]"); + assertThat(result, notNullValue()); + assertThat(result, hasSize(0)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"[]"}) + void testHandlerWithEmptyInputs(final String handle) { + String result = converter.handler(handle, Collections.emptyList()); + assertEquals("[]", result); + } + + @Test + void testHandlerWithValidHandleAndEmptyAliveList() { + DivideUpstream upstream = buildDivideUpstream("http://", "localhost:8080", true, 100, 10); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(upstream)); + + String result = converter.handler(handle, Collections.emptyList()); + + assertNotNull(result); + assertFalse(result.isEmpty()); + List resultList = GsonUtils.getInstance().fromList(result, DivideUpstream.class); + assertThat(resultList, hasSize(1)); + } + + @Test + void testHandlerWithEmptyHandleAndAliveList() { + CommonUpstream aliveUpstream = buildCommonUpstream("http://", "localhost:8080"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler("", aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DivideUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:8080", resultList.get(0).getUpstreamUrl()); + } + + @Test + void testHandlerWithExistingAndNewAliveUpstreams() { + DivideUpstream existingUpstream = buildDivideUpstream("http://", "localhost:8080", true, 100, 10); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("http://", "localhost:8080"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("http://", "localhost:8081"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DivideUpstream.class); + assertThat(resultList, hasSize(2)); + assertTrue(resultList.stream().anyMatch(u -> "localhost:8080".equals(u.getUpstreamUrl()))); + assertTrue(resultList.stream().anyMatch(u -> "localhost:8081".equals(u.getUpstreamUrl()))); + } + + @Test + void testHandlerWithDeadUpstream() { + DivideUpstream existingUpstream1 = buildDivideUpstream("http://", "localhost:8080", true, 100, 10); + DivideUpstream existingUpstream2 = buildDivideUpstream("http://", "localhost:8081", true, 50, 5); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("http://", "localhost:8080"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DivideUpstream.class); + assertThat(resultList, hasSize(2)); + + DivideUpstream alive = resultList.stream() + .filter(u -> "localhost:8080".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(alive); + assertTrue(alive.isStatus()); + + DivideUpstream dead = resultList.stream() + .filter(u -> "localhost:8081".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(dead); + assertFalse(dead.isStatus()); + } + + @Test + void testDoHandleWithMultipleScenarios() { + DivideUpstream existingUpstream = buildDivideUpstream("http://", "localhost:8080", false, 100, 10); + existingUpstream.setTimestamp(System.currentTimeMillis()); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("http://", "localhost:8080"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("https://", "localhost:8082"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DivideUpstream.class); + assertThat(resultList, hasSize(2)); + + DivideUpstream revived = resultList.stream() + .filter(u -> "localhost:8080".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(revived); + assertTrue(revived.isStatus()); + + DivideUpstream newUpstream = resultList.stream() + .filter(u -> "localhost:8082".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(newUpstream); + assertEquals("https://", newUpstream.getProtocol()); + } + + @Test + void testHandlerPreservesWeightAndWarmup() { + DivideUpstream existingUpstream = buildDivideUpstream("http://", "localhost:8080", true, 80, 20); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream = buildCommonUpstream("http://", "localhost:8080"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + List resultList = GsonUtils.getInstance().fromList(result, DivideUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals(80, resultList.get(0).getWeight()); + assertEquals(20, resultList.get(0).getWarmup()); + } + + private DivideUpstream buildDivideUpstream(final String protocol, final String url, + final boolean status, final int weight, final int warmup) { + return DivideUpstream.builder() + .protocol(protocol) + .upstreamHost("localhost") + .upstreamUrl(url) + .status(status) + .weight(weight) + .warmup(warmup) + .timestamp(System.currentTimeMillis()) + .build(); + } + + private CommonUpstream buildCommonUpstream(final String protocol, final String url) { + return new CommonUpstream(protocol, "localhost", url, true, System.currentTimeMillis()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/DubboSelectorHandleConverterTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/DubboSelectorHandleConverterTest.java new file mode 100644 index 000000000000..6f284dda77f2 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/DubboSelectorHandleConverterTest.java @@ -0,0 +1,250 @@ +/* + * 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 org.apache.shenyu.admin.service.converter; + +import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; +import org.apache.shenyu.common.dto.convert.selector.DubboUpstream; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link DubboSelectorHandleConverter}. + */ +class DubboSelectorHandleConverterTest { + + private DubboSelectorHandleConverter converter; + + @BeforeEach + void setUp() { + converter = new DubboSelectorHandleConverter(); + } + + @Test + void testPluginName() { + String pluginName = converter.pluginName(); + assertThat(pluginName, is(PluginEnum.DUBBO.getName())); + assertEquals("dubbo", pluginName); + } + + @Test + void testConvertUpstream() { + DubboUpstream upstream1 = buildDubboUpstream("dubbo://", "localhost:20880", true, 100, 10); + DubboUpstream upstream2 = buildDubboUpstream("dubbo://", "localhost:20881", true, 50, 5); + List upstreamList = new ArrayList<>(); + upstreamList.add(upstream1); + upstreamList.add(upstream2); + + String handle = GsonUtils.getInstance().toJson(upstreamList); + List result = converter.convertUpstream(handle); + + assertThat(result, notNullValue()); + assertThat(result, hasSize(2)); + assertEquals("dubbo://", result.get(0).getProtocol()); + assertEquals("localhost:20880", result.get(0).getUpstreamUrl()); + assertTrue(result.get(0).isStatus()); + } + + @Test + void testConvertUpstreamWithEmptyHandle() { + List result = converter.convertUpstream("[]"); + assertThat(result, notNullValue()); + assertThat(result, hasSize(0)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"[]"}) + void testHandlerWithEmptyInputs(final String handle) { + String result = converter.handler(handle, Collections.emptyList()); + assertEquals("[]", result); + } + + @Test + void testHandlerWithValidHandleAndEmptyAliveList() { + DubboUpstream upstream = buildDubboUpstream("dubbo://", "localhost:20880", true, 100, 10); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(upstream)); + + String result = converter.handler(handle, Collections.emptyList()); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(0)); + } + + @Test + void testHandlerWithEmptyHandleAndAliveList() { + CommonUpstream aliveUpstream = buildCommonUpstream("dubbo://", "localhost:20880"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler("", aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:20880", resultList.get(0).getUpstreamUrl()); + } + + @Test + void testHandlerWithExistingAndNewAliveUpstreams() { + DubboUpstream existingUpstream = buildDubboUpstream("dubbo://", "localhost:20880", true, 100, 10); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("dubbo://", "localhost:20880"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("dubbo://", "localhost:20881"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(2)); + assertTrue(resultList.stream().anyMatch(u -> "localhost:20880".equals(u.getUpstreamUrl()))); + assertTrue(resultList.stream().anyMatch(u -> "localhost:20881".equals(u.getUpstreamUrl()))); + } + + @Test + void testHandlerRemovesDeadUpstream() { + DubboUpstream existingUpstream1 = buildDubboUpstream("dubbo://", "localhost:20880", true, 100, 10); + DubboUpstream existingUpstream2 = buildDubboUpstream("dubbo://", "localhost:20881", true, 50, 5); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("dubbo://", "localhost:20880"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:20880", resultList.get(0).getUpstreamUrl()); + assertTrue(resultList.get(0).isStatus()); + } + + @Test + void testHandlerWithMultipleScenarios() { + DubboUpstream existingUpstream = buildDubboUpstream("dubbo://", "localhost:20880", false, 100, 10); + existingUpstream.setTimestamp(System.currentTimeMillis()); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("dubbo://", "localhost:20880"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("dubbo://", "localhost:20882"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(2)); + + DubboUpstream revived = resultList.stream() + .filter(u -> "localhost:20880".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(revived); + assertTrue(revived.isStatus()); + + DubboUpstream newUpstream = resultList.stream() + .filter(u -> "localhost:20882".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(newUpstream); + assertEquals("dubbo://", newUpstream.getProtocol()); + } + + @Test + void testHandlerPreservesWeightAndWarmup() { + DubboUpstream existingUpstream = buildDubboUpstream("dubbo://", "localhost:20880", true, 80, 20); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream = buildCommonUpstream("dubbo://", "localhost:20880"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals(80, resultList.get(0).getWeight()); + assertEquals(20, resultList.get(0).getWarmup()); + } + + @Test + void testHandlerRemovesAllDeadUpstreams() { + DubboUpstream existingUpstream1 = buildDubboUpstream("dubbo://", "localhost:20880", true, 100, 10); + DubboUpstream existingUpstream2 = buildDubboUpstream("dubbo://", "localhost:20881", true, 50, 5); + DubboUpstream existingUpstream3 = buildDubboUpstream("dubbo://", "localhost:20882", true, 60, 8); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + existingList.add(existingUpstream3); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("dubbo://", "localhost:20881"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, DubboUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:20881", resultList.get(0).getUpstreamUrl()); + assertFalse(resultList.stream().anyMatch(u -> "localhost:20880".equals(u.getUpstreamUrl()))); + assertFalse(resultList.stream().anyMatch(u -> "localhost:20882".equals(u.getUpstreamUrl()))); + } + + private DubboUpstream buildDubboUpstream(final String protocol, final String url, + final boolean status, final int weight, final int warmup) { + return DubboUpstream.builder() + .protocol(protocol) + .upstreamHost("localhost") + .upstreamUrl(url) + .status(status) + .weight(weight) + .warmup(warmup) + .timestamp(System.currentTimeMillis()) + .build(); + } + + private CommonUpstream buildCommonUpstream(final String protocol, final String url) { + return new CommonUpstream(protocol, "localhost", url, true, System.currentTimeMillis()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/GrpcSelectorHandleConverterTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/GrpcSelectorHandleConverterTest.java new file mode 100644 index 000000000000..7a04f02dc105 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/GrpcSelectorHandleConverterTest.java @@ -0,0 +1,243 @@ +/* + * 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 org.apache.shenyu.admin.service.converter; + +import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; +import org.apache.shenyu.common.dto.convert.selector.GrpcUpstream; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link GrpcSelectorHandleConverter}. + */ +class GrpcSelectorHandleConverterTest { + + private GrpcSelectorHandleConverter converter; + + @BeforeEach + void setUp() { + converter = new GrpcSelectorHandleConverter(); + } + + @Test + void testPluginName() { + String pluginName = converter.pluginName(); + assertThat(pluginName, is(PluginEnum.GRPC.getName())); + assertEquals("grpc", pluginName); + } + + @Test + void testConvertUpstream() { + GrpcUpstream upstream1 = buildGrpcUpstream("localhost:9090", true, 100); + GrpcUpstream upstream2 = buildGrpcUpstream("localhost:9091", true, 50); + List upstreamList = new ArrayList<>(); + upstreamList.add(upstream1); + upstreamList.add(upstream2); + + String handle = GsonUtils.getInstance().toJson(upstreamList); + List result = converter.convertUpstream(handle); + + assertThat(result, notNullValue()); + assertThat(result, hasSize(2)); + assertEquals("localhost:9090", result.get(0).getUpstreamUrl()); + assertTrue(result.get(0).isStatus()); + } + + @Test + void testConvertUpstreamWithEmptyHandle() { + List result = converter.convertUpstream("[]"); + assertThat(result, notNullValue()); + assertThat(result, hasSize(0)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"[]"}) + void testHandlerWithEmptyInputs(final String handle) { + String result = converter.handler(handle, Collections.emptyList()); + assertEquals("[]", result); + } + + @Test + void testHandlerWithValidHandleAndEmptyAliveList() { + GrpcUpstream upstream = buildGrpcUpstream("localhost:9090", true, 100); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(upstream)); + + String result = converter.handler(handle, Collections.emptyList()); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(0)); + } + + @Test + void testHandlerWithEmptyHandleAndAliveList() { + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:9090"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler("", aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:9090", resultList.get(0).getUpstreamUrl()); + } + + @Test + void testHandlerWithExistingAndNewAliveUpstreams() { + GrpcUpstream existingUpstream = buildGrpcUpstream("localhost:9090", true, 100); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("localhost:9090"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("localhost:9091"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(2)); + assertTrue(resultList.stream().anyMatch(u -> "localhost:9090".equals(u.getUpstreamUrl()))); + assertTrue(resultList.stream().anyMatch(u -> "localhost:9091".equals(u.getUpstreamUrl()))); + } + + @Test + void testHandlerRemovesDeadUpstream() { + GrpcUpstream existingUpstream1 = buildGrpcUpstream("localhost:9090", true, 100); + GrpcUpstream existingUpstream2 = buildGrpcUpstream("localhost:9091", true, 50); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:9090"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:9090", resultList.get(0).getUpstreamUrl()); + assertTrue(resultList.get(0).isStatus()); + } + + @Test + void testHandlerWithMultipleScenarios() { + GrpcUpstream existingUpstream = buildGrpcUpstream("localhost:9090", false, 100); + existingUpstream.setTimestamp(System.currentTimeMillis()); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("localhost:9090"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("localhost:9092"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(2)); + + GrpcUpstream revived = resultList.stream() + .filter(u -> "localhost:9090".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(revived); + assertTrue(revived.isStatus()); + + GrpcUpstream newUpstream = resultList.stream() + .filter(u -> "localhost:9092".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(newUpstream); + } + + @Test + void testHandlerPreservesWeight() { + GrpcUpstream existingUpstream = buildGrpcUpstream("localhost:9090", true, 80); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:9090"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals(80, resultList.get(0).getWeight()); + } + + @Test + void testHandlerRemovesAllDeadUpstreams() { + GrpcUpstream existingUpstream1 = buildGrpcUpstream("localhost:9090", true, 100); + GrpcUpstream existingUpstream2 = buildGrpcUpstream("localhost:9091", true, 50); + GrpcUpstream existingUpstream3 = buildGrpcUpstream("localhost:9092", true, 60); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + existingList.add(existingUpstream3); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:9091"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, GrpcUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:9091", resultList.get(0).getUpstreamUrl()); + assertFalse(resultList.stream().anyMatch(u -> "localhost:9090".equals(u.getUpstreamUrl()))); + assertFalse(resultList.stream().anyMatch(u -> "localhost:9092".equals(u.getUpstreamUrl()))); + } + + private GrpcUpstream buildGrpcUpstream(final String url, final boolean status, final int weight) { + return GrpcUpstream.builder() + .upstreamUrl(url) + .status(status) + .weight(weight) + .timestamp(System.currentTimeMillis()) + .build(); + } + + private CommonUpstream buildCommonUpstream(final String url) { + return new CommonUpstream("", "localhost", url, true, System.currentTimeMillis()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/SelectorHandleConverterFactorTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/SelectorHandleConverterFactorTest.java new file mode 100644 index 000000000000..8250885d8b54 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/SelectorHandleConverterFactorTest.java @@ -0,0 +1,96 @@ +/* + * 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 org.apache.shenyu.admin.service.converter; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.HashMap; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertSame; + +/** + * Test case for {@link SelectorHandleConverterFactor}. + */ +class SelectorHandleConverterFactorTest { + + private SelectorHandleConverterFactor factor; + + private DivideSelectorHandleConverter divideConverter; + + private DubboSelectorHandleConverter dubboConverter; + + private GrpcSelectorHandleConverter grpcConverter; + + @BeforeEach + void setUp() { + divideConverter = new DivideSelectorHandleConverter(); + dubboConverter = new DubboSelectorHandleConverter(); + grpcConverter = new GrpcSelectorHandleConverter(); + + Map maps = new HashMap<>(); + maps.put("divide", divideConverter); + maps.put("dubbo", dubboConverter); + maps.put("grpc", grpcConverter); + + factor = new SelectorHandleConverterFactor(maps); + } + + @ParameterizedTest + @CsvSource({ + "divide,divide", + "dubbo,dubbo", + "grpc,grpc" + }) + void testNewInstanceWithValidPlugins(final String pluginName, final String expectedPluginName) { + SelectorHandleConverter converter = factor.newInstance(pluginName); + assertNotNull(converter); + assertEquals(expectedPluginName, converter.pluginName()); + + if ("divide".equals(pluginName)) { + assertSame(divideConverter, converter); + } else if ("dubbo".equals(pluginName)) { + assertSame(dubboConverter, converter); + } else if ("grpc".equals(pluginName)) { + assertSame(grpcConverter, converter); + } + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"nonexistent", "unknown", "invalid"}) + void testNewInstanceWithInvalidPlugins(final String pluginName) { + SelectorHandleConverter converter = factor.newInstance(pluginName); + assertNull(converter); + } + + @Test + void testFactorWithEmptyMap() { + SelectorHandleConverterFactor emptyFactor = new SelectorHandleConverterFactor(new HashMap<>()); + SelectorHandleConverter converter = emptyFactor.newInstance("divide"); + assertNull(converter); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/TarsSelectorHandleConverterTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/TarsSelectorHandleConverterTest.java new file mode 100644 index 000000000000..42a42318dcd1 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/converter/TarsSelectorHandleConverterTest.java @@ -0,0 +1,246 @@ +/* + * 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 org.apache.shenyu.admin.service.converter; + +import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; +import org.apache.shenyu.common.dto.convert.selector.TarsUpstream; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.utils.GsonUtils; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.notNullValue; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * Test case for {@link TarsSelectorHandleConverter}. + */ +class TarsSelectorHandleConverterTest { + + private TarsSelectorHandleConverter converter; + + @BeforeEach + void setUp() { + converter = new TarsSelectorHandleConverter(); + } + + @Test + void testPluginName() { + String pluginName = converter.pluginName(); + assertThat(pluginName, is(PluginEnum.TARS.getName())); + assertEquals("tars", pluginName); + } + + @Test + void testConvertUpstream() { + TarsUpstream upstream1 = buildTarsUpstream("localhost:8080", true, 100, 10); + TarsUpstream upstream2 = buildTarsUpstream("localhost:8081", true, 50, 5); + List upstreamList = new ArrayList<>(); + upstreamList.add(upstream1); + upstreamList.add(upstream2); + + String handle = GsonUtils.getInstance().toJson(upstreamList); + List result = converter.convertUpstream(handle); + + assertThat(result, notNullValue()); + assertThat(result, hasSize(2)); + assertEquals("localhost:8080", result.get(0).getUpstreamUrl()); + assertTrue(result.get(0).isStatus()); + } + + @Test + void testConvertUpstreamWithEmptyHandle() { + List result = converter.convertUpstream("[]"); + assertThat(result, notNullValue()); + assertThat(result, hasSize(0)); + } + + @ParameterizedTest + @NullAndEmptySource + @ValueSource(strings = {"[]"}) + void testHandlerWithEmptyInputs(final String handle) { + String result = converter.handler(handle, Collections.emptyList()); + assertEquals("[]", result); + } + + @Test + void testHandlerWithValidHandleAndEmptyAliveList() { + TarsUpstream upstream = buildTarsUpstream("localhost:8080", true, 100, 10); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(upstream)); + + String result = converter.handler(handle, Collections.emptyList()); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(0)); + } + + @Test + void testHandlerWithEmptyHandleAndAliveList() { + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:8080"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler("", aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:8080", resultList.get(0).getUpstreamUrl()); + } + + @Test + void testHandlerWithExistingAndNewAliveUpstreams() { + TarsUpstream existingUpstream = buildTarsUpstream("localhost:8080", true, 100, 10); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("localhost:8080"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("localhost:8081"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(2)); + assertTrue(resultList.stream().anyMatch(u -> "localhost:8080".equals(u.getUpstreamUrl()))); + assertTrue(resultList.stream().anyMatch(u -> "localhost:8081".equals(u.getUpstreamUrl()))); + } + + @Test + void testHandlerRemovesDeadUpstream() { + TarsUpstream existingUpstream1 = buildTarsUpstream("localhost:8080", true, 100, 10); + TarsUpstream existingUpstream2 = buildTarsUpstream("localhost:8081", true, 50, 5); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:8080"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:8080", resultList.get(0).getUpstreamUrl()); + assertTrue(resultList.get(0).isStatus()); + } + + @Test + void testHandlerWithMultipleScenarios() { + TarsUpstream existingUpstream = buildTarsUpstream("localhost:8080", false, 100, 10); + existingUpstream.setTimestamp(System.currentTimeMillis()); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream1 = buildCommonUpstream("localhost:8080"); + CommonUpstream aliveUpstream2 = buildCommonUpstream("localhost:8082"); + List aliveList = new ArrayList<>(); + aliveList.add(aliveUpstream1); + aliveList.add(aliveUpstream2); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(2)); + + TarsUpstream revived = resultList.stream() + .filter(u -> "localhost:8080".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(revived); + assertTrue(revived.isStatus()); + + TarsUpstream newUpstream = resultList.stream() + .filter(u -> "localhost:8082".equals(u.getUpstreamUrl())) + .findFirst() + .orElse(null); + assertNotNull(newUpstream); + } + + @Test + void testHandlerPreservesWeightAndWarmup() { + TarsUpstream existingUpstream = buildTarsUpstream("localhost:8080", true, 80, 20); + String handle = GsonUtils.getInstance().toJson(Collections.singletonList(existingUpstream)); + + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:8080"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals(80, resultList.get(0).getWeight()); + assertEquals(20, resultList.get(0).getWarmup()); + } + + @Test + void testHandlerRemovesAllDeadUpstreams() { + TarsUpstream existingUpstream1 = buildTarsUpstream("localhost:8080", true, 100, 10); + TarsUpstream existingUpstream2 = buildTarsUpstream("localhost:8081", true, 50, 5); + TarsUpstream existingUpstream3 = buildTarsUpstream("localhost:8082", true, 60, 8); + List existingList = new ArrayList<>(); + existingList.add(existingUpstream1); + existingList.add(existingUpstream2); + existingList.add(existingUpstream3); + String handle = GsonUtils.getInstance().toJson(existingList); + + CommonUpstream aliveUpstream = buildCommonUpstream("localhost:8081"); + List aliveList = Collections.singletonList(aliveUpstream); + + String result = converter.handler(handle, aliveList); + + assertNotNull(result); + List resultList = GsonUtils.getInstance().fromList(result, TarsUpstream.class); + assertThat(resultList, hasSize(1)); + assertEquals("localhost:8081", resultList.get(0).getUpstreamUrl()); + assertFalse(resultList.stream().anyMatch(u -> "localhost:8080".equals(u.getUpstreamUrl()))); + assertFalse(resultList.stream().anyMatch(u -> "localhost:8082".equals(u.getUpstreamUrl()))); + } + + private TarsUpstream buildTarsUpstream(final String url, final boolean status, + final int weight, final int warmup) { + return TarsUpstream.builder() + .upstreamUrl(url) + .status(status) + .weight(weight) + .warmup(warmup) + .timestamp(System.currentTimeMillis()) + .build(); + } + + private CommonUpstream buildCommonUpstream(final String url) { + return new CommonUpstream("", "localhost", url, true, System.currentTimeMillis()); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/DictEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/DictEventPublisherTest.java new file mode 100644 index 000000000000..890064d92faf --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/DictEventPublisherTest.java @@ -0,0 +1,158 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.ShenyuDictDO; +import org.apache.shenyu.admin.model.event.dict.BatchDictDeletedEvent; +import org.apache.shenyu.admin.model.event.dict.DictCreatedEvent; +import org.apache.shenyu.admin.model.event.dict.DictUpdatedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link DictEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class DictEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private DictEventPublisher dictEventPublisher; + + @BeforeEach + void setUp() { + dictEventPublisher = new DictEventPublisher(applicationEventPublisher); + } + + @Test + void testOnCreated() { + ShenyuDictDO dict = buildShenyuDictDO("1", "test-type", "test-code"); + + dictEventPublisher.onCreated(dict); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DictCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + DictCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(dict, event.getDict()); + } + + @Test + void testOnUpdated() { + ShenyuDictDO before = buildShenyuDictDO("1", "test-type", "old-code"); + ShenyuDictDO after = buildShenyuDictDO("1", "test-type", "new-code"); + + dictEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DictUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + DictUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @ParameterizedTest + @MethodSource("provideDictListsForDeleted") + void testOnDeleted(final List dictList, final int expectedSize) { + dictEventPublisher.onDeleted(dictList); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchDictDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchDictDeletedEvent event = captor.getValue(); + assertNotNull(event); + assertNotNull(event.getDeletedIds()); + assertEquals(expectedSize, event.getDeletedIds().size()); + + if (expectedSize == 1) { + assertTrue(event.getDeletedIds().contains("1")); + } + } + + private static Stream provideDictListsForDeleted() { + ShenyuDictDO dict1 = buildShenyuDictDO("1", "test-type", "test-code"); + ShenyuDictDO dict2 = buildShenyuDictDO("1", "test-type", "code1"); + ShenyuDictDO dict3 = buildShenyuDictDO("2", "test-type", "code2"); + ShenyuDictDO dict4 = buildShenyuDictDO("3", "test-type", "code3"); + + return Stream.of( + Arguments.of(Collections.singletonList(dict1), 1), + Arguments.of(Arrays.asList(dict2, dict3, dict4), 3), + Arguments.of(Collections.emptyList(), 0) + ); + } + + @Test + void testPublish() { + ShenyuDictDO dict = buildShenyuDictDO("1", "test-type", "test-code"); + DictCreatedEvent event = new DictCreatedEvent(dict, "test-user"); + + dictEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + @Test + void testMultipleOperations() { + ShenyuDictDO dict1 = buildShenyuDictDO("1", "type1", "code1"); + ShenyuDictDO dict2 = buildShenyuDictDO("2", "type2", "code2"); + ShenyuDictDO dict3 = buildShenyuDictDO("3", "type3", "code3"); + + dictEventPublisher.onCreated(dict1); + dictEventPublisher.onUpdated(dict2, dict1); + dictEventPublisher.onDeleted(Collections.singletonList(dict3)); + + verify(applicationEventPublisher, times(3)).publishEvent(any()); + } + + private static ShenyuDictDO buildShenyuDictDO(final String id, final String type, final String dictCode) { + ShenyuDictDO dict = new ShenyuDictDO(); + dict.setId(id); + dict.setType(type); + dict.setDictCode(dictCode); + dict.setDictName("test-name"); + dict.setDictValue("test-value"); + dict.setEnabled(true); + dict.setSort(1); + return dict; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/MetaDataEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/MetaDataEventPublisherTest.java new file mode 100644 index 000000000000..cc3fcfde0c97 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/MetaDataEventPublisherTest.java @@ -0,0 +1,165 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.MetaDataDO; +import org.apache.shenyu.admin.model.event.metadata.MetaDataCreatedEvent; +import org.apache.shenyu.admin.model.event.metadata.MetadataUpdatedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link MetaDataEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class MetaDataEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private MetaDataEventPublisher metaDataEventPublisher; + + @BeforeEach + void setUp() { + metaDataEventPublisher = new MetaDataEventPublisher(applicationEventPublisher); + } + + @Test + void testOnCreated() { + MetaDataDO metaData = buildMetaDataDO("1", "test-path", "test-service"); + + metaDataEventPublisher.onCreated(metaData); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MetaDataCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + MetaDataCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(metaData, event.getMetaData()); + } + + @Test + void testOnUpdated() { + MetaDataDO before = buildMetaDataDO("1", "old-path", "test-service"); + MetaDataDO after = buildMetaDataDO("1", "new-path", "test-service"); + + metaDataEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(MetadataUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + MetadataUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @ParameterizedTest + @MethodSource("provideMetaDataListsForDeleted") + void testOnDeleted(final List metaDataList) { + metaDataEventPublisher.onDeleted(metaDataList); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + private static Stream> provideMetaDataListsForDeleted() { + MetaDataDO metaData1 = buildMetaDataDO("1", "test-path", "test-service"); + MetaDataDO metaData2 = buildMetaDataDO("1", "path1", "service1"); + MetaDataDO metaData3 = buildMetaDataDO("2", "path2", "service2"); + MetaDataDO metaData4 = buildMetaDataDO("3", "path3", "service3"); + + return Stream.of( + Collections.singletonList(metaData1), + Arrays.asList(metaData2, metaData3, metaData4), + Collections.emptyList() + ); + } + + @ParameterizedTest + @MethodSource("provideMetaDataListsForEnabled") + void testOnEnabled(final List metaDataList) { + metaDataEventPublisher.onEnabled(metaDataList); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + private static Stream> provideMetaDataListsForEnabled() { + MetaDataDO metaData1 = buildMetaDataDO("1", "test-path", "test-service"); + MetaDataDO metaData2 = buildMetaDataDO("1", "path1", "service1"); + MetaDataDO metaData3 = buildMetaDataDO("2", "path2", "service2"); + + return Stream.of( + Collections.singletonList(metaData1), + Arrays.asList(metaData2, metaData3), + Collections.emptyList() + ); + } + + @Test + void testPublish() { + MetaDataDO metaData = buildMetaDataDO("1", "test-path", "test-service"); + MetaDataCreatedEvent event = new MetaDataCreatedEvent(metaData, "test-user"); + + metaDataEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + @Test + void testMultipleOperations() { + MetaDataDO metaData1 = buildMetaDataDO("1", "path1", "service1"); + MetaDataDO metaData2 = buildMetaDataDO("2", "path2", "service2"); + MetaDataDO metaData3 = buildMetaDataDO("3", "path3", "service3"); + + metaDataEventPublisher.onCreated(metaData1); + metaDataEventPublisher.onUpdated(metaData2, metaData1); + metaDataEventPublisher.onDeleted(Collections.singletonList(metaData3)); + metaDataEventPublisher.onEnabled(Collections.singletonList(metaData1)); + + verify(applicationEventPublisher, times(6)).publishEvent(any()); + } + + private static MetaDataDO buildMetaDataDO(final String id, final String path, final String serviceName) { + MetaDataDO metaData = new MetaDataDO(); + metaData.setId(id); + metaData.setPath(path); + metaData.setServiceName(serviceName); + metaData.setMethodName("testMethod"); + metaData.setRpcType("http"); + metaData.setEnabled(true); + return metaData; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/NamespaceEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/NamespaceEventPublisherTest.java new file mode 100644 index 000000000000..85c339c6edf8 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/NamespaceEventPublisherTest.java @@ -0,0 +1,65 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.event.AdminDataModelChangedEvent; +import org.apache.shenyu.admin.model.enums.EventTypeEnum; +import org.apache.shenyu.admin.model.vo.NamespaceVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link NamespaceEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class NamespaceEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private NamespaceEventPublisher namespaceEventPublisher; + + @BeforeEach + void setUp() { + namespaceEventPublisher = new NamespaceEventPublisher(applicationEventPublisher); + } + + @Test + void testPublish() { + NamespaceVO namespaceVO = new NamespaceVO(); + namespaceVO.setNamespaceId("test-namespace-id"); + AdminDataModelChangedEvent event = new TestNamespaceEvent(namespaceVO); + + namespaceEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static class TestNamespaceEvent extends AdminDataModelChangedEvent { + TestNamespaceEvent(final NamespaceVO namespaceVO) { + super(namespaceVO, null, EventTypeEnum.NAMESPACE_CREATE, "test-user"); + } + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/NamespacePluginEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/NamespacePluginEventPublisherTest.java new file mode 100644 index 000000000000..828088f7666a --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/NamespacePluginEventPublisherTest.java @@ -0,0 +1,162 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.event.plugin.BatchNamespacePluginChangedEvent; +import org.apache.shenyu.admin.model.event.plugin.BatchNamespacePluginDeletedEvent; +import org.apache.shenyu.admin.model.event.plugin.NamespacePluginChangedEvent; +import org.apache.shenyu.admin.model.event.plugin.NamespacePluginCreatedEvent; +import org.apache.shenyu.admin.model.vo.NamespacePluginVO; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link NamespacePluginEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class NamespacePluginEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private NamespacePluginEventPublisher namespacePluginEventPublisher; + + @BeforeEach + void setUp() { + namespacePluginEventPublisher = new NamespacePluginEventPublisher(applicationEventPublisher); + } + + @Test + void testOnCreated() { + NamespacePluginVO plugin = buildNamespacePluginVO("1", "test-plugin", "test-namespace"); + + namespacePluginEventPublisher.onCreated(plugin); + + ArgumentCaptor captor = ArgumentCaptor.forClass(NamespacePluginCreatedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + NamespacePluginCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(plugin, event.getPlugin()); + } + + @Test + void testOnUpdated() { + NamespacePluginVO before = buildNamespacePluginVO("1", "old-plugin", "test-namespace"); + NamespacePluginVO after = buildNamespacePluginVO("1", "new-plugin", "test-namespace"); + + namespacePluginEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(NamespacePluginChangedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + NamespacePluginChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnDeletedSingle() { + NamespacePluginVO plugin = buildNamespacePluginVO("1", "test-plugin", "test-namespace"); + + namespacePluginEventPublisher.onDeleted(plugin); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedCollection() { + NamespacePluginVO plugin1 = buildNamespacePluginVO("1", "plugin1", "test-namespace"); + NamespacePluginVO plugin2 = buildNamespacePluginVO("2", "plugin2", "test-namespace"); + List plugins = Arrays.asList(plugin1, plugin2); + + namespacePluginEventPublisher.onDeleted(plugins); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchNamespacePluginDeletedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchNamespacePluginDeletedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(2, event.getDeletedPluginIds().size()); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptyList = Collections.emptyList(); + + namespacePluginEventPublisher.onDeleted(emptyList); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnEnabled() { + NamespacePluginVO plugin1 = buildNamespacePluginVO("1", "plugin1", "test-namespace"); + NamespacePluginVO plugin2 = buildNamespacePluginVO("2", "plugin2", "test-namespace"); + List plugins = Arrays.asList(plugin1, plugin2); + + namespacePluginEventPublisher.onEnabled(plugins); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchNamespacePluginChangedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchNamespacePluginChangedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testPublish() { + NamespacePluginVO plugin = buildNamespacePluginVO("1", "test-plugin", "test-namespace"); + NamespacePluginCreatedEvent event = new NamespacePluginCreatedEvent(plugin, "test-user"); + + namespacePluginEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static NamespacePluginVO buildNamespacePluginVO(final String id, final String name, final String namespaceId) { + NamespacePluginVO plugin = new NamespacePluginVO(); + plugin.setId(id); + plugin.setName(name); + plugin.setNamespaceId(namespaceId); + plugin.setEnabled(true); + plugin.setRole("test-role"); + plugin.setConfig("test-config"); + return plugin; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/PluginEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/PluginEventPublisherTest.java new file mode 100644 index 000000000000..b44942741c73 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/PluginEventPublisherTest.java @@ -0,0 +1,157 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.PluginDO; +import org.apache.shenyu.admin.model.event.plugin.BatchPluginChangedEvent; +import org.apache.shenyu.admin.model.event.plugin.BatchPluginDeletedEvent; +import org.apache.shenyu.admin.model.event.plugin.PluginChangedEvent; +import org.apache.shenyu.admin.model.event.plugin.PluginCreatedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link PluginEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class PluginEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private PluginEventPublisher pluginEventPublisher; + + @BeforeEach + void setUp() { + pluginEventPublisher = new PluginEventPublisher(applicationEventPublisher); + } + + @Test + void testOnCreated() { + PluginDO plugin = buildPluginDO("1", "test-plugin"); + + pluginEventPublisher.onCreated(plugin); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PluginCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + PluginCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(plugin, event.getPlugin()); + } + + @Test + void testOnUpdated() { + PluginDO before = buildPluginDO("1", "old-plugin"); + PluginDO after = buildPluginDO("1", "new-plugin"); + + pluginEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PluginChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + PluginChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnDeletedSingle() { + PluginDO plugin = buildPluginDO("1", "test-plugin"); + + pluginEventPublisher.onDeleted(plugin); + + verify(applicationEventPublisher, times(1)).publishEvent(any()); + } + + @Test + void testOnDeletedCollection() { + PluginDO plugin1 = buildPluginDO("1", "plugin1"); + PluginDO plugin2 = buildPluginDO("2", "plugin2"); + List plugins = Arrays.asList(plugin1, plugin2); + + pluginEventPublisher.onDeleted(plugins); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchPluginDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchPluginDeletedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(2, event.getDeletedPluginIds().size()); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptyList = Collections.emptyList(); + + pluginEventPublisher.onDeleted(emptyList); + + verify(applicationEventPublisher, times(1)).publishEvent(any()); + } + + @Test + void testOnEnabled() { + PluginDO plugin1 = buildPluginDO("1", "plugin1"); + PluginDO plugin2 = buildPluginDO("2", "plugin2"); + List plugins = Arrays.asList(plugin1, plugin2); + + pluginEventPublisher.onEnabled(plugins); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchPluginChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchPluginChangedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testPublish() { + PluginDO plugin = buildPluginDO("1", "test-plugin"); + PluginCreatedEvent event = new PluginCreatedEvent(plugin, "test-user"); + + pluginEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static PluginDO buildPluginDO(final String id, final String name) { + PluginDO plugin = new PluginDO(); + plugin.setId(id); + plugin.setName(name); + plugin.setEnabled(true); + plugin.setRole("test-role"); + plugin.setConfig("test-config"); + return plugin; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/PluginHandleEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/PluginHandleEventPublisherTest.java new file mode 100644 index 000000000000..0683b319ee66 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/PluginHandleEventPublisherTest.java @@ -0,0 +1,138 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.PluginHandleDO; +import org.apache.shenyu.admin.model.event.handle.BatchPluginHandleChangedEvent; +import org.apache.shenyu.admin.model.event.handle.PluginHandleChangedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link PluginHandleEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class PluginHandleEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private PluginHandleEventPublisher pluginHandleEventPublisher; + + @BeforeEach + void setUp() { + pluginHandleEventPublisher = new PluginHandleEventPublisher(applicationEventPublisher); + } + + @Test + void testOnCreated() { + PluginHandleDO pluginHandle = buildPluginHandleDO("1", "test-field"); + + pluginHandleEventPublisher.onCreated(pluginHandle); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PluginHandleChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + PluginHandleChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(pluginHandle, event.getAfter()); + } + + @Test + void testOnUpdated() { + PluginHandleDO before = buildPluginHandleDO("1", "old-field"); + PluginHandleDO after = buildPluginHandleDO("1", "new-field"); + + pluginHandleEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(PluginHandleChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + PluginHandleChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnDeletedSingle() { + PluginHandleDO pluginHandle = buildPluginHandleDO("1", "test-field"); + + pluginHandleEventPublisher.onDeleted(pluginHandle); + + verify(applicationEventPublisher, times(1)).publishEvent(any()); + } + + @Test + void testOnDeletedCollection() { + PluginHandleDO handle1 = buildPluginHandleDO("1", "field1"); + PluginHandleDO handle2 = buildPluginHandleDO("2", "field2"); + List handles = Arrays.asList(handle1, handle2); + + pluginHandleEventPublisher.onDeleted(handles); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchPluginHandleChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchPluginHandleChangedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptyList = Collections.emptyList(); + + pluginHandleEventPublisher.onDeleted(emptyList); + + verify(applicationEventPublisher, times(1)).publishEvent(any()); + } + + @Test + void testPublish() { + PluginHandleDO pluginHandle = buildPluginHandleDO("1", "test-field"); + PluginHandleChangedEvent event = new PluginHandleChangedEvent(pluginHandle, null, null, "test-user"); + + pluginHandleEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static PluginHandleDO buildPluginHandleDO(final String id, final String field) { + PluginHandleDO pluginHandle = new PluginHandleDO(); + pluginHandle.setId(id); + pluginHandle.setField(field); + pluginHandle.setPluginId("test-plugin-id"); + pluginHandle.setLabel("test-label"); + return pluginHandle; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/ResourceEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/ResourceEventPublisherTest.java new file mode 100644 index 000000000000..98471d44943e --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/ResourceEventPublisherTest.java @@ -0,0 +1,156 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.ResourceDO; +import org.apache.shenyu.admin.model.event.resource.BatchResourceCreatedEvent; +import org.apache.shenyu.admin.model.event.resource.BatchResourceDeletedEvent; +import org.apache.shenyu.admin.model.event.resource.ResourceChangedEvent; +import org.apache.shenyu.admin.model.event.resource.ResourceCreatedEvent; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link ResourceEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class ResourceEventPublisherTest { + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private ResourceEventPublisher resourceEventPublisher; + + @BeforeEach + void setUp() { + resourceEventPublisher = new ResourceEventPublisher(applicationEventPublisher); + } + + @Test + void testOnCreatedSingle() { + ResourceDO resource = buildResourceDO("1", "test-resource"); + + resourceEventPublisher.onCreated(resource); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + ResourceCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(resource, event.getResource()); + } + + @Test + void testOnCreatedCollection() { + ResourceDO resource1 = buildResourceDO("1", "resource1"); + ResourceDO resource2 = buildResourceDO("2", "resource2"); + List resources = Arrays.asList(resource1, resource2); + + resourceEventPublisher.onCreated(resources); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchResourceCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchResourceCreatedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testOnCreatedEmptyCollection() { + List emptyList = Collections.emptyList(); + + resourceEventPublisher.onCreated(emptyList); + + verify(applicationEventPublisher, times(1)).publishEvent(any()); + } + + @Test + void testOnUpdated() { + ResourceDO before = buildResourceDO("1", "old-resource"); + ResourceDO after = buildResourceDO("1", "new-resource"); + + resourceEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(ResourceChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + ResourceChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnDeleted() { + ResourceDO resource1 = buildResourceDO("1", "resource1"); + ResourceDO resource2 = buildResourceDO("2", "resource2"); + List resources = Arrays.asList(resource1, resource2); + + resourceEventPublisher.onDeleted(resources); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchResourceDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchResourceDeletedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(2, event.getDeletedIds().size()); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptyList = Collections.emptyList(); + + resourceEventPublisher.onDeleted(emptyList); + + verify(applicationEventPublisher, times(1)).publishEvent(any()); + } + + @Test + void testPublish() { + ResourceDO resource = buildResourceDO("1", "test-resource"); + ResourceCreatedEvent event = new ResourceCreatedEvent(resource, "test-user"); + + resourceEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static ResourceDO buildResourceDO(final String id, final String name) { + ResourceDO resource = new ResourceDO(); + resource.setId(id); + resource.setName(name); + resource.setTitle("test-title"); + resource.setUrl("/test/url"); + return resource; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/RoleEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/RoleEventPublisherTest.java new file mode 100644 index 000000000000..1ae1eb2377f8 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/RoleEventPublisherTest.java @@ -0,0 +1,179 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.RoleDO; +import org.apache.shenyu.admin.model.event.role.BatchRoleDeletedEvent; +import org.apache.shenyu.admin.model.event.role.RoleCreatedEvent; +import org.apache.shenyu.admin.model.event.role.RoleUpdatedEvent; +import org.apache.shenyu.admin.utils.SessionUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link RoleEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class RoleEventPublisherTest { + + private static final String TEST_OPERATOR = "test-operator"; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private RoleEventPublisher roleEventPublisher; + + private MockedStatic sessionUtilMockedStatic; + + @BeforeEach + void setUp() { + roleEventPublisher = new RoleEventPublisher(applicationEventPublisher); + sessionUtilMockedStatic = mockStatic(SessionUtil.class); + sessionUtilMockedStatic.when(SessionUtil::visitorName).thenReturn(TEST_OPERATOR); + } + + @AfterEach + void tearDown() { + sessionUtilMockedStatic.close(); + } + + @Test + void testOnCreated() { + RoleDO role = buildRoleDO("1", "admin", "Administrator role"); + + roleEventPublisher.onCreated(role); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RoleCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + RoleCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(role, event.getRole()); + } + + @Test + void testOnUpdated() { + RoleDO before = buildRoleDO("1", "admin", "Old description"); + RoleDO after = buildRoleDO("1", "admin", "New description"); + List permissions = Arrays.asList("permission1", "permission2"); + + roleEventPublisher.onUpdated(after, before, permissions); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RoleUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + RoleUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getRole()); + assertEquals(permissions, event.getNewPermission()); + } + + @Test + void testOnUpdatedWithEmptyPermissions() { + RoleDO before = buildRoleDO("1", "admin", "Old description"); + RoleDO after = buildRoleDO("1", "admin", "New description"); + List emptyPermissions = Collections.emptyList(); + + roleEventPublisher.onUpdated(after, before, emptyPermissions); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RoleUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + RoleUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getRole()); + assertEquals(emptyPermissions, event.getNewPermission()); + } + + @Test + void testOnDeleted() { + RoleDO role1 = buildRoleDO("1", "admin", "Admin role"); + RoleDO role2 = buildRoleDO("2", "user", "User role"); + List roles = Arrays.asList(role1, role2); + + roleEventPublisher.onDeleted(roles); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchRoleDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchRoleDeletedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testOnDeletedWithEmptyCollection() { + List emptyList = Collections.emptyList(); + + roleEventPublisher.onDeleted(emptyList); + + verify(applicationEventPublisher, times(1)).publishEvent(ArgumentCaptor.forClass(BatchRoleDeletedEvent.class).capture()); + } + + @Test + void testOnDeletedWithSingleRole() { + RoleDO role = buildRoleDO("1", "admin", "Admin role"); + List roles = Collections.singletonList(role); + + roleEventPublisher.onDeleted(roles); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchRoleDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchRoleDeletedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testPublish() { + RoleDO role = buildRoleDO("1", "admin", "Admin role"); + RoleCreatedEvent event = new RoleCreatedEvent(role, TEST_OPERATOR); + + roleEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static RoleDO buildRoleDO(final String id, final String roleName, final String description) { + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + return RoleDO.builder() + .id(id) + .roleName(roleName) + .description(description) + .dateCreated(currentTime) + .dateUpdated(currentTime) + .build(); + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/RuleEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/RuleEventPublisherTest.java new file mode 100644 index 000000000000..eed1f1afd0fc --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/RuleEventPublisherTest.java @@ -0,0 +1,437 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.listener.DataChangedEvent; +import org.apache.shenyu.admin.mapper.RuleConditionMapper; +import org.apache.shenyu.admin.mapper.RuleMapper; +import org.apache.shenyu.admin.model.dto.RuleConditionDTO; +import org.apache.shenyu.admin.model.entity.PluginDO; +import org.apache.shenyu.admin.model.entity.RuleConditionDO; +import org.apache.shenyu.admin.model.entity.RuleDO; +import org.apache.shenyu.admin.model.event.rule.RuleChangedEvent; +import org.apache.shenyu.admin.model.event.rule.RuleCreatedEvent; +import org.apache.shenyu.admin.model.event.rule.RuleUpdatedEvent; +import org.apache.shenyu.admin.model.event.selector.BatchSelectorDeletedEvent; +import org.apache.shenyu.admin.utils.SessionUtil; +import org.apache.shenyu.common.enums.ConfigGroupEnum; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anySet; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Test case for {@link RuleEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class RuleEventPublisherTest { + + private static final String TEST_OPERATOR = "test-operator"; + + private static final String TEST_PLUGIN_NAME = "test-plugin"; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + @Mock + private RuleConditionMapper ruleConditionMapper; + + @Mock + private RuleMapper ruleMapper; + + private RuleEventPublisher ruleEventPublisher; + + private MockedStatic sessionUtilMockedStatic; + + @BeforeEach + void setUp() { + ruleEventPublisher = new RuleEventPublisher(applicationEventPublisher, ruleConditionMapper, ruleMapper); + sessionUtilMockedStatic = mockStatic(SessionUtil.class); + sessionUtilMockedStatic.when(SessionUtil::visitorName).thenReturn(TEST_OPERATOR); + } + + @AfterEach + void tearDown() { + sessionUtilMockedStatic.close(); + } + + @Test + void testOnCreatedSimple() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + + ruleEventPublisher.onCreated(rule); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RuleCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + RuleCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(rule, event.getRule()); + } + + @Test + void testOnCreatedWithCondition() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + List conditions = Arrays.asList( + buildRuleConditionDTO("1", "1", "header", "=", "test", "value1") + ); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onCreated(rule, conditions); + + ArgumentCaptor createdCaptor = ArgumentCaptor.forClass(RuleCreatedEvent.class); + ArgumentCaptor dataChangedCaptor = ArgumentCaptor.forClass(DataChangedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher).publishEvent(createdCaptor.capture()); + verify(applicationEventPublisher).publishEvent(dataChangedCaptor.capture()); + + RuleCreatedEvent createdEvent = createdCaptor.getValue(); + assertNotNull(createdEvent); + assertEquals(rule, createdEvent.getRule()); + + DataChangedEvent dataChangedEvent = dataChangedCaptor.getValue(); + assertNotNull(dataChangedEvent); + assertEquals(ConfigGroupEnum.RULE, dataChangedEvent.getGroupKey()); + } + + @Test + void testOnUpdatedSimple() { + RuleDO before = buildRuleDO("1", "selector1", "old-rule"); + RuleDO after = buildRuleDO("1", "selector1", "new-rule"); + + ruleEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RuleUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + RuleUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnUpdatedWithCondition() { + RuleDO before = buildRuleDO("1", "selector1", "old-rule"); + RuleDO after = buildRuleDO("1", "selector1", "new-rule"); + List conditions = Arrays.asList( + buildRuleConditionDTO("1", "1", "header", "=", "test", "value1") + ); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onUpdated(after, before, conditions); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnUpdatedWithFullConditions() { + RuleDO before = buildRuleDO("1", "selector1", "old-rule"); + RuleDO after = buildRuleDO("1", "selector1", "new-rule"); + List conditions = Arrays.asList( + buildRuleConditionDTO("1", "1", "header", "=", "test", "value1") + ); + List beforeConditions = Arrays.asList( + buildRuleConditionDO("1", "1", "header", "=", "test", "oldValue") + ); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onUpdated(after, before, conditions, beforeConditions); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedSingle() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + + ruleEventPublisher.onDeleted(rule); + + ArgumentCaptor captor = ArgumentCaptor.forClass(RuleChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + RuleChangedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testOnDeletedCollection() { + RuleDO rule1 = buildRuleDO("1", "selector1", "rule1"); + RuleDO rule2 = buildRuleDO("2", "selector1", "rule2"); + List rules = Arrays.asList(rule1, rule2); + + List conditions = Arrays.asList( + buildRuleConditionDO("1", "1", "header", "=", "test", "value1"), + buildRuleConditionDO("2", "2", "header", "=", "test", "value2") + ); + + when(ruleConditionMapper.selectByRuleIdSet(anySet())).thenReturn(conditions); + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onDeleted(rules); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedWithBatchSelectorDeletedEvent() { + RuleDO rule1 = buildRuleDO("1", "selector1", "rule1"); + RuleDO rule2 = buildRuleDO("2", "selector2", "rule2"); + List rules = Arrays.asList(rule1, rule2); + + PluginDO plugin = buildPluginDO("plugin1", TEST_PLUGIN_NAME); + BatchSelectorDeletedEvent selectorEvent = new BatchSelectorDeletedEvent( + Collections.emptyList(), TEST_OPERATOR, Collections.singletonList(plugin) + ); + + List conditions = Arrays.asList( + buildRuleConditionDO("1", "1", "header", "=", "test", "value1") + ); + + when(ruleConditionMapper.selectByRuleIdSet(anySet())).thenReturn(conditions); + + ruleEventPublisher.onDeleted(rules, selectorEvent); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptyRules = Collections.emptyList(); + + when(ruleConditionMapper.selectByRuleIdSet(anySet())).thenReturn(Collections.emptyList()); + + ruleEventPublisher.onDeleted(emptyRules); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnRegister() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + List conditions = Arrays.asList( + buildRuleConditionDTO("1", "1", "header", "=", "test", "value1") + ); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onRegister(rule, conditions); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DataChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + DataChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(ConfigGroupEnum.RULE, event.getGroupKey()); + } + + @Test + void testPublish() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + RuleCreatedEvent event = new RuleCreatedEvent(rule, TEST_OPERATOR); + + ruleEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + @Test + void testOnCreatedWithEmptyCondition() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + List emptyConditions = Collections.emptyList(); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onCreated(rule, emptyConditions); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnUpdatedWithEmptyConditions() { + RuleDO before = buildRuleDO("1", "selector1", "old-rule"); + RuleDO after = buildRuleDO("1", "selector1", "new-rule"); + List emptyConditions = Collections.emptyList(); + List emptyBeforeConditions = Collections.emptyList(); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onUpdated(after, before, emptyConditions, emptyBeforeConditions); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedWithBatchSelectorDeletedEventNullPlugin() { + RuleDO rule1 = buildRuleDO("1", "selector1", "rule1"); + RuleDO rule2 = buildRuleDO("2", "selector2", "rule2"); + List rules = Arrays.asList(rule1, rule2); + + BatchSelectorDeletedEvent selectorEvent = new BatchSelectorDeletedEvent( + Collections.emptyList(), TEST_OPERATOR, Collections.emptyList() + ); + + List conditions = Arrays.asList( + buildRuleConditionDO("1", "1", "header", "=", "test", "value1") + ); + + when(ruleConditionMapper.selectByRuleIdSet(anySet())).thenReturn(conditions); + + ruleEventPublisher.onDeleted(rules, selectorEvent); + + ArgumentCaptor dataChangedCaptor = ArgumentCaptor.forClass(DataChangedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher).publishEvent(dataChangedCaptor.capture()); + + DataChangedEvent event = dataChangedCaptor.getValue(); + assertNotNull(event); + assertEquals(ConfigGroupEnum.RULE, event.getGroupKey()); + } + + @Test + void testOnDeletedCollectionWithNoConditions() { + RuleDO rule1 = buildRuleDO("1", "selector1", "rule1"); + List rules = Collections.singletonList(rule1); + + when(ruleConditionMapper.selectByRuleIdSet(anySet())).thenReturn(Collections.emptyList()); + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onDeleted(rules); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnRegisterWithEmptyConditions() { + RuleDO rule = buildRuleDO("1", "selector1", "test-rule"); + List emptyConditions = Collections.emptyList(); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onRegister(rule, emptyConditions); + + ArgumentCaptor captor = ArgumentCaptor.forClass(DataChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + DataChangedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(ConfigGroupEnum.RULE, event.getGroupKey()); + } + + @Test + void testOnDeletedCollectionWithMultipleConditionsPerRule() { + RuleDO rule1 = buildRuleDO("1", "selector1", "rule1"); + List rules = Collections.singletonList(rule1); + + List conditions = Arrays.asList( + buildRuleConditionDO("1", "1", "header", "=", "test", "value1"), + buildRuleConditionDO("2", "1", "query", "match", "param", "value2"), + buildRuleConditionDO("3", "1", "uri", "=", "path", "/api") + ); + + when(ruleConditionMapper.selectByRuleIdSet(anySet())).thenReturn(conditions); + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onDeleted(rules); + + ArgumentCaptor dataChangedCaptor = ArgumentCaptor.forClass(DataChangedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher).publishEvent(dataChangedCaptor.capture()); + + DataChangedEvent event = dataChangedCaptor.getValue(); + assertNotNull(event); + assertEquals(ConfigGroupEnum.RULE, event.getGroupKey()); + } + + @Test + void testOnUpdatedWithConditionAndEmptyBeforeCondition() { + RuleDO before = buildRuleDO("1", "selector1", "old-rule"); + RuleDO after = buildRuleDO("1", "selector1", "new-rule"); + List conditions = Arrays.asList( + buildRuleConditionDTO("1", "1", "header", "=", "test", "value1") + ); + + when(ruleMapper.getPluginNameBySelectorId(anyString())).thenReturn(TEST_PLUGIN_NAME); + + ruleEventPublisher.onUpdated(after, before, conditions, Collections.emptyList()); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + private static RuleDO buildRuleDO(final String id, final String selectorId, final String ruleName) { + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + RuleDO ruleDO = RuleDO.builder() + .id(id) + .selectorId(selectorId) + .ruleName(ruleName) + .matchMode(0) + .enabled(true) + .loged(false) + .sortCode(1) + .handle("{}") + .matchRestful(false) + .build(); + ruleDO.setDateCreated(currentTime); + ruleDO.setDateUpdated(currentTime); + return ruleDO; + } + + private static RuleConditionDTO buildRuleConditionDTO(final String id, final String ruleId, + final String paramType, final String operator, + final String paramName, final String paramValue) { + return new RuleConditionDTO(id, ruleId, paramType, operator, paramName, paramValue); + } + + private static RuleConditionDO buildRuleConditionDO(final String id, final String ruleId, + final String paramType, final String operator, + final String paramName, final String paramValue) { + RuleConditionDO conditionDO = new RuleConditionDO(ruleId, paramType, operator, paramName, paramValue); + conditionDO.setId(id); + return conditionDO; + } + + private static PluginDO buildPluginDO(final String id, final String name) { + PluginDO pluginDO = new PluginDO(); + pluginDO.setId(id); + pluginDO.setName(name); + return pluginDO; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/SelectorEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/SelectorEventPublisherTest.java new file mode 100644 index 000000000000..fa68c12a2a05 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/SelectorEventPublisherTest.java @@ -0,0 +1,225 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.listener.DataChangedEvent; +import org.apache.shenyu.admin.model.entity.PluginDO; +import org.apache.shenyu.admin.model.entity.SelectorDO; +import org.apache.shenyu.admin.model.event.selector.SelectorChangedEvent; +import org.apache.shenyu.admin.model.event.selector.SelectorCreatedEvent; +import org.apache.shenyu.admin.model.event.selector.SelectorUpdatedEvent; +import org.apache.shenyu.admin.service.impl.UpstreamCheckService; +import org.apache.shenyu.admin.utils.SessionUtil; +import org.apache.shenyu.common.enums.ConfigGroupEnum; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link SelectorEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class SelectorEventPublisherTest { + + private static final String TEST_OPERATOR = "test-operator"; + + private static final String TEST_PLUGIN_NAME = "test-plugin"; + + private static final String DIVIDE_PLUGIN_NAME = "divide"; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private SelectorEventPublisher selectorEventPublisher; + + private MockedStatic sessionUtilMockedStatic; + + private MockedStatic upstreamCheckServiceMockedStatic; + + @BeforeEach + void setUp() { + selectorEventPublisher = new SelectorEventPublisher(applicationEventPublisher); + sessionUtilMockedStatic = mockStatic(SessionUtil.class); + sessionUtilMockedStatic.when(SessionUtil::visitorName).thenReturn(TEST_OPERATOR); + upstreamCheckServiceMockedStatic = mockStatic(UpstreamCheckService.class); + } + + @AfterEach + void tearDown() { + sessionUtilMockedStatic.close(); + upstreamCheckServiceMockedStatic.close(); + } + + @Test + void testOnCreated() { + SelectorDO selector = buildSelectorDO("1", "plugin1", "test-selector"); + + selectorEventPublisher.onCreated(selector); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectorCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + SelectorCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(selector, event.getSelector()); + } + + @Test + void testOnUpdated() { + SelectorDO before = buildSelectorDO("1", "plugin1", "old-selector"); + SelectorDO after = buildSelectorDO("1", "plugin1", "new-selector"); + + selectorEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectorUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + SelectorUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnDeletedSingle() { + SelectorDO selector = buildSelectorDO("1", "plugin1", "test-selector"); + + selectorEventPublisher.onDeleted(selector); + + ArgumentCaptor captor = ArgumentCaptor.forClass(SelectorChangedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + SelectorChangedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testOnDeletedCollection() { + SelectorDO selector1 = buildSelectorDO("1", "plugin1", "selector1"); + SelectorDO selector2 = buildSelectorDO("2", "plugin1", "selector2"); + List selectors = Arrays.asList(selector1, selector2); + + PluginDO plugin = buildPluginDO("plugin1", TEST_PLUGIN_NAME); + List plugins = Collections.singletonList(plugin); + + selectorEventPublisher.onDeleted(selectors, plugins); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedCollectionWithDividePlugin() { + SelectorDO selector1 = buildSelectorDO("1", "plugin1", "selector1"); + SelectorDO selector2 = buildSelectorDO("2", "plugin1", "selector2"); + List selectors = Arrays.asList(selector1, selector2); + + PluginDO plugin = buildPluginDO("plugin1", DIVIDE_PLUGIN_NAME); + List plugins = Collections.singletonList(plugin); + + selectorEventPublisher.onDeleted(selectors, plugins); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + + upstreamCheckServiceMockedStatic.verify(() -> UpstreamCheckService.removeByKey("1"), times(1)); + upstreamCheckServiceMockedStatic.verify(() -> UpstreamCheckService.removeByKey("2"), times(1)); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptySelectors = Collections.emptyList(); + List emptyPlugins = Collections.emptyList(); + + selectorEventPublisher.onDeleted(emptySelectors, emptyPlugins); + + verify(applicationEventPublisher, times(2)).publishEvent(any()); + } + + @Test + void testOnDeletedCollectionWithMultiplePlugins() { + SelectorDO selector1 = buildSelectorDO("1", "plugin1", "selector1"); + SelectorDO selector2 = buildSelectorDO("2", "plugin2", "selector2"); + List selectors = Arrays.asList(selector1, selector2); + + PluginDO plugin1 = buildPluginDO("plugin1", TEST_PLUGIN_NAME); + PluginDO plugin2 = buildPluginDO("plugin2", "another-plugin"); + List plugins = Arrays.asList(plugin1, plugin2); + + selectorEventPublisher.onDeleted(selectors, plugins); + + ArgumentCaptor dataChangedCaptor = ArgumentCaptor.forClass(DataChangedEvent.class); + verify(applicationEventPublisher, times(2)).publishEvent(any()); + verify(applicationEventPublisher).publishEvent(dataChangedCaptor.capture()); + + DataChangedEvent event = dataChangedCaptor.getValue(); + assertNotNull(event); + assertEquals(ConfigGroupEnum.SELECTOR, event.getGroupKey()); + } + + @Test + void testPublish() { + SelectorDO selector = buildSelectorDO("1", "plugin1", "test-selector"); + SelectorCreatedEvent event = new SelectorCreatedEvent(selector, TEST_OPERATOR); + + selectorEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static SelectorDO buildSelectorDO(final String id, final String pluginId, final String selectorName) { + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + SelectorDO selectorDO = SelectorDO.builder() + .id(id) + .pluginId(pluginId) + .selectorName(selectorName) + .matchMode(0) + .enabled(true) + .loged(false) + .sortCode(1) + .handle("{}") + .matchRestful(false) + .build(); + selectorDO.setDateCreated(currentTime); + selectorDO.setDateUpdated(currentTime); + return selectorDO; + } + + private static PluginDO buildPluginDO(final String id, final String name) { + PluginDO pluginDO = new PluginDO(); + pluginDO.setId(id); + pluginDO.setName(name); + return pluginDO; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/UserEventPublisherTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/UserEventPublisherTest.java new file mode 100644 index 000000000000..789792ff97f4 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/service/publish/UserEventPublisherTest.java @@ -0,0 +1,153 @@ +/* + * 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 org.apache.shenyu.admin.service.publish; + +import org.apache.shenyu.admin.model.entity.DashboardUserDO; +import org.apache.shenyu.admin.model.event.user.BatchUserDeletedEvent; +import org.apache.shenyu.admin.model.event.user.UserCreatedEvent; +import org.apache.shenyu.admin.model.event.user.UserUpdatedEvent; +import org.apache.shenyu.admin.utils.SessionUtil; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.context.ApplicationEventPublisher; + +import java.sql.Timestamp; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +/** + * Test case for {@link UserEventPublisher}. + */ +@ExtendWith(MockitoExtension.class) +class UserEventPublisherTest { + + private static final String TEST_OPERATOR = "test-operator"; + + @Mock + private ApplicationEventPublisher applicationEventPublisher; + + private UserEventPublisher userEventPublisher; + + private MockedStatic sessionUtilMockedStatic; + + @BeforeEach + void setUp() { + userEventPublisher = new UserEventPublisher(applicationEventPublisher); + sessionUtilMockedStatic = mockStatic(SessionUtil.class); + sessionUtilMockedStatic.when(SessionUtil::visitorName).thenReturn(TEST_OPERATOR); + } + + @AfterEach + void tearDown() { + sessionUtilMockedStatic.close(); + } + + @Test + void testOnCreated() { + DashboardUserDO user = buildDashboardUserDO("1", "testuser", "password"); + + userEventPublisher.onCreated(user); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserCreatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + UserCreatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(user, event.getChangedUser()); + } + + @Test + void testOnUpdated() { + DashboardUserDO before = buildDashboardUserDO("1", "olduser", "oldpassword"); + DashboardUserDO after = buildDashboardUserDO("1", "newuser", "newpassword"); + + userEventPublisher.onUpdated(after, before); + + ArgumentCaptor captor = ArgumentCaptor.forClass(UserUpdatedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + UserUpdatedEvent event = captor.getValue(); + assertNotNull(event); + assertEquals(after, event.getAfter()); + assertEquals(before, event.getBefore()); + } + + @Test + void testOnDeleted() { + DashboardUserDO user1 = buildDashboardUserDO("1", "user1", "password1"); + DashboardUserDO user2 = buildDashboardUserDO("2", "user2", "password2"); + List users = Arrays.asList(user1, user2); + + userEventPublisher.onDeleted(users); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchUserDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchUserDeletedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testOnDeletedEmptyCollection() { + List emptyUsers = Collections.emptyList(); + + userEventPublisher.onDeleted(emptyUsers); + + ArgumentCaptor captor = ArgumentCaptor.forClass(BatchUserDeletedEvent.class); + verify(applicationEventPublisher, times(1)).publishEvent(captor.capture()); + + BatchUserDeletedEvent event = captor.getValue(); + assertNotNull(event); + } + + @Test + void testPublish() { + DashboardUserDO user = buildDashboardUserDO("1", "testuser", "password"); + UserCreatedEvent event = new UserCreatedEvent(user, TEST_OPERATOR); + + userEventPublisher.publish(event); + + verify(applicationEventPublisher, times(1)).publishEvent(event); + } + + private static DashboardUserDO buildDashboardUserDO(final String id, final String userName, final String password) { + Timestamp currentTime = new Timestamp(System.currentTimeMillis()); + DashboardUserDO userDO = new DashboardUserDO(); + userDO.setId(id); + userDO.setUserName(userName); + userDO.setPassword(password); + userDO.setRole(1); + userDO.setEnabled(true); + userDO.setDateCreated(currentTime); + userDO.setDateUpdated(currentTime); + return userDO; + } +} diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/BaseTriggerTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/BaseTriggerTest.java index 9022bb8c5bad..77f9f53b6d64 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/BaseTriggerTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/BaseTriggerTest.java @@ -21,11 +21,18 @@ import org.junit.jupiter.api.Test; import java.sql.PreparedStatement; +import java.sql.SQLException; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** - * BaseTriggerTest. + * Test case for {@link BaseTrigger}. */ public class BaseTriggerTest { @@ -38,4 +45,139 @@ public void sqlExecute() { newRow[3] = new Object(); Assertions.assertDoesNotThrow(() -> BaseTrigger.sqlExecute(newRow, mock(PreparedStatement.class))); } + + @Test + public void testSqlExecuteWithNullFirstElement() throws SQLException { + // Test case where newRow[0] is null - should execute SQL + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {null, "value1", "value2", "value3", "value4"}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Verify that setObject was called for the UUID at position 1 + verify(mockStatement).setObject(eq(1), any(String.class)); + // Verify that setObject was called for each element in the loop (i=1 to length-3) + // newRow.length = 5, so loop from i=1 to 5-3=2, so i=1,2 + verify(mockStatement).setObject(2, "value1"); + verify(mockStatement).setObject(3, "value2"); + // Verify executeUpdate was called + verify(mockStatement).executeUpdate(); + } + + @Test + public void testSqlExecuteWithEmptyStringFirstElement() throws SQLException { + // Test case where newRow[0] is empty string - should execute SQL + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {"", "value1", "value2", "value3", "value4"}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Verify that setObject was called for the UUID at position 1 + verify(mockStatement).setObject(eq(1), any(String.class)); + // Verify that setObject was called for each element in the loop + verify(mockStatement).setObject(2, "value1"); + verify(mockStatement).setObject(3, "value2"); + // Verify executeUpdate was called + verify(mockStatement).executeUpdate(); + } + + @Test + public void testSqlExecuteWithNonEmptyFirstElement() throws SQLException { + // Test case where newRow[0] is not empty - should do nothing + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {"not-empty", "value1", "value2", "value3", "value4"}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Verify that no methods were called on preparedStatement + verify(mockStatement, never()).setObject(anyInt(), any()); + verify(mockStatement, never()).executeUpdate(); + } + + @Test + public void testSqlExecuteWithMinimumArrayLength() throws SQLException { + // Test with minimum array length where loop doesn't execute + // newRow.length = 3, loop from i=1 to 3-3=0, so no loop iterations + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {null, "value1", "value2"}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Verify UUID was set + verify(mockStatement).setObject(eq(1), any(String.class)); + // Verify no setObject calls in the loop (since length-2 = 3-2 = 1, loop doesn't run) + verify(mockStatement, never()).setObject(eq(2), any()); + verify(mockStatement, never()).setObject(eq(3), any()); + // Verify executeUpdate was called + verify(mockStatement).executeUpdate(); + } + + @Test + public void testSqlExecuteWithLargerArray() throws SQLException { + // Test with larger array + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {null, "val1", "val2", "val3", "val4", "val5", "val6"}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Verify UUID was set + verify(mockStatement).setObject(eq(1), any(String.class)); + // newRow.length = 7, loop from i=1 to 7-3=4, so i=1,2,3,4 + verify(mockStatement).setObject(2, "val1"); + verify(mockStatement).setObject(3, "val2"); + verify(mockStatement).setObject(4, "val3"); + verify(mockStatement).setObject(5, "val4"); + // i=5 would be 7-2=5, but loop condition is i < length-2, so i<5, stops at i=4 + verify(mockStatement, never()).setObject(eq(6), any()); + verify(mockStatement, never()).setObject(eq(7), any()); + // Verify executeUpdate was called + verify(mockStatement).executeUpdate(); + } + + @Test + public void testSqlExecuteWithDifferentDataTypes() throws SQLException { + // Test with different data types in the array + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {null, "string", 123, true, 45.67}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Verify UUID was set + verify(mockStatement).setObject(eq(1), any(String.class)); + // newRow.length = 5, loop from i=1 to 5-3=2, so i=1,2 + verify(mockStatement).setObject(2, "string"); + verify(mockStatement).setObject(3, 123); + // Verify executeUpdate was called + verify(mockStatement).executeUpdate(); + } + + @Test + public void testSqlExecuteThrowsSQLException() throws SQLException { + // Test that SQLException is propagated + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {null, "value1"}; + + // Mock PreparedStatement to throw SQLException + doThrow(new SQLException("Test exception")).when(mockStatement).executeUpdate(); + + Assertions.assertThrows(SQLException.class, () -> { + BaseTrigger.sqlExecute(newRow, mockStatement); + }); + } + + @Test + public void testSqlExecuteWithEmptyArray() throws SQLException { + // Test edge case - this would cause ArrayIndexOutOfBoundsException + // but let's see what happens with minimal array + PreparedStatement mockStatement = mock(PreparedStatement.class); + Object[] newRow = {null}; + + BaseTrigger.sqlExecute(newRow, mockStatement); + + // Should still work since we only access newRow[0] and check length + verify(mockStatement).setObject(eq(1), any(String.class)); + // length = 1, loop from i=1 to 1-3=-2, so no loop iterations + verify(mockStatement, never()).setObject(eq(2), any()); + verify(mockStatement).executeUpdate(); + } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/CommonUpstreamUtilsTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/CommonUpstreamUtilsTest.java index 2a8168fa007d..fe6b7f6be889 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/CommonUpstreamUtilsTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/CommonUpstreamUtilsTest.java @@ -17,11 +17,14 @@ package org.apache.shenyu.admin.utils; +import org.apache.shenyu.admin.model.dto.DiscoveryUpstreamDTO; +import org.apache.shenyu.common.constant.Constants; +import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; import org.apache.shenyu.common.dto.convert.selector.DivideUpstream; -import org.apache.shenyu.common.dto.convert.selector.WebSocketUpstream; import org.apache.shenyu.common.dto.convert.selector.DubboUpstream; import org.apache.shenyu.common.dto.convert.selector.GrpcUpstream; -import org.apache.shenyu.common.dto.convert.selector.CommonUpstream; +import org.apache.shenyu.common.dto.convert.selector.TarsUpstream; +import org.apache.shenyu.common.dto.convert.selector.WebSocketUpstream; import org.apache.shenyu.register.common.enums.EventType; import org.junit.Assert; import org.junit.Test; @@ -133,4 +136,222 @@ public void buildUrl() { Assert.assertEquals(HOST + ":" + PORT, url); } + @Test + public void testBuildDefaultDivideUpstreamWithNullPort() { + // Test case: null port - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDefaultDivideUpstream(HOST, null, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + Assert.assertEquals("localhost", result.getUpstreamHost()); + Assert.assertEquals("http://", result.getProtocol()); + } + + @Test + public void testBuildDefaultDivideUpstreamWithBlankHost() { + // Test case: blank host - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDefaultDivideUpstream("", PORT, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDefaultDivideUpstreamWithNullHostAndPort() { + // Test case: null host and port + DivideUpstream result = CommonUpstreamUtils.buildDefaultDivideUpstream(null, null, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDefaultDiscoveryUpstreamDTO() { + // Test case: normal case + String protocol = "http"; + DiscoveryUpstreamDTO result = CommonUpstreamUtils.buildDefaultDiscoveryUpstreamDTO(HOST, PORT, protocol, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertEquals(HOST + ":" + PORT, result.getUrl()); + Assert.assertEquals(protocol, result.getProtocol()); + Assert.assertEquals(0, result.getStatus()); + Assert.assertEquals(Long.valueOf(50), Long.valueOf(result.getWeight())); + Assert.assertEquals(SYS_DEFAULT_NAMESPACE_ID, result.getNamespaceId()); + Assert.assertNotNull(result.getProps()); + } + + @Test + public void testBuildDefaultDiscoveryUpstreamDTOWithGrpcProtocol() { + // Test case: grpc protocol + String protocol = "grpc"; + DiscoveryUpstreamDTO result = CommonUpstreamUtils.buildDefaultDiscoveryUpstreamDTO(HOST, PORT, protocol, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertEquals(protocol, result.getProtocol()); + } + + @Test + public void testBuildDivideUpstreamWithDeletedEvent() { + // Test case: DELETED event - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDivideUpstream("http://", HOST, PORT, SYS_DEFAULT_NAMESPACE_ID, EventType.DELETED); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDivideUpstreamWithOfflineEvent() { + // Test case: OFFLINE event - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDivideUpstream("http://", HOST, PORT, SYS_DEFAULT_NAMESPACE_ID, EventType.OFFLINE); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDivideUpstreamWithIgnoredEvent() { + // Test case: IGNORED event - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDivideUpstream("http://", HOST, PORT, SYS_DEFAULT_NAMESPACE_ID, EventType.IGNORED); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDivideUpstreamWithNullPort() { + // Test case: null port - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDivideUpstream("http://", HOST, null, SYS_DEFAULT_NAMESPACE_ID, EventType.REGISTER); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDivideUpstreamWithBlankHost() { + // Test case: blank host - should mark status as false + DivideUpstream result = CommonUpstreamUtils.buildDivideUpstream("http://", "", PORT, SYS_DEFAULT_NAMESPACE_ID, EventType.REGISTER); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildWebSocketUpstreamWithNullPort() { + // Test case: null port - should mark status as false + WebSocketUpstream result = CommonUpstreamUtils.buildWebSocketUpstream("ws://", HOST, null, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + Assert.assertEquals("localhost", result.getHost()); + } + + @Test + public void testBuildWebSocketUpstreamWithBlankHost() { + // Test case: blank host - should mark status as false + WebSocketUpstream result = CommonUpstreamUtils.buildWebSocketUpstream("ws://", "", PORT, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDefaultDubboUpstreamWithNullPort() { + // Test case: null port - should mark status as false + DubboUpstream result = CommonUpstreamUtils.buildDefaultDubboUpstream(HOST, null); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + Assert.assertEquals("dubbo://", result.getProtocol()); + } + + @Test + public void testBuildDefaultDubboUpstreamWithBlankHost() { + // Test case: blank host - should mark status as false + DubboUpstream result = CommonUpstreamUtils.buildDefaultDubboUpstream("", PORT); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDefaultGrpcUpstreamWithNullPort() { + // Test case: null port - should mark status as false + GrpcUpstream result = CommonUpstreamUtils.buildDefaultGrpcUpstream(HOST, null, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDefaultGrpcUpstreamWithBlankHost() { + // Test case: blank host - should mark status as false + GrpcUpstream result = CommonUpstreamUtils.buildDefaultGrpcUpstream("", PORT, SYS_DEFAULT_NAMESPACE_ID); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildDefaultTarsUpstream() { + // Test case: valid data + TarsUpstream result = CommonUpstreamUtils.buildDefaultTarsUpstream(HOST, PORT); + Assert.assertNotNull(result); + Assert.assertEquals(HOST + ":" + PORT, result.getUpstreamUrl()); + Assert.assertEquals(Long.valueOf(50), Long.valueOf(result.getWeight())); + Assert.assertEquals(Long.valueOf(Constants.WARMUP_TIME), Long.valueOf(result.getWarmup())); + Assert.assertTrue(result.isStatus()); + } + + @Test + public void testBuildDefaultTarsUpstreamWithNullPort() { + // Test case: null port - should mark status as false + TarsUpstream result = CommonUpstreamUtils.buildDefaultTarsUpstream(HOST, null); + Assert.assertNotNull(result); + Assert.assertFalse(result.isStatus()); + } + + @Test + public void testBuildAliveTarsUpstream() { + // Test case: normal case + String upstreamUrl = HOST + ":" + PORT; + TarsUpstream result = CommonUpstreamUtils.buildAliveTarsUpstream(upstreamUrl); + Assert.assertNotNull(result); + Assert.assertEquals(upstreamUrl, result.getUpstreamUrl()); + Assert.assertEquals(Long.valueOf(50), Long.valueOf(result.getWeight())); + } + + @Test + public void testConvertCommonUpstreamListWithHealthCheck() { + // Test case: list with health check enabled + WebSocketUpstream upstream = WebSocketUpstream.builder() + .protocol("ws://") + .host("localhost") + .upstreamUrl("ws://localhost:8080") + .status(true) + .timestamp(System.currentTimeMillis()) + .build(); + upstream.setHealthCheckEnabled(true); + + List upstreamList = new ArrayList<>(); + upstreamList.add(upstream); + + List result = CommonUpstreamUtils.convertCommonUpstreamList(upstreamList); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertTrue(result.get(0).isHealthCheckEnabled()); + } + + @Test + public void testBuildDefaultAliveDivideUpstream() { + // Test case: normal case + DivideUpstream result = CommonUpstreamUtils.buildDefaultAliveDivideUpstream(HOST); + Assert.assertNotNull(result); + Assert.assertEquals("localhost", result.getUpstreamHost()); + Assert.assertEquals("http://", result.getProtocol()); + Assert.assertEquals(HOST, result.getUpstreamUrl()); + Assert.assertEquals(Long.valueOf(50), Long.valueOf(result.getWeight())); + } + + @Test + public void testBuildUrlWithZeroPort() { + // Test case: zero port + String url = CommonUpstreamUtils.buildUrl(HOST, 0); + Assert.assertEquals(HOST + ":0", url); + } + + @Test + public void testBuildDefaultDivideUpstreamTimestamp() { + // Test case: verify timestamp is set + long beforeTime = System.currentTimeMillis(); + DivideUpstream result = CommonUpstreamUtils.buildDefaultDivideUpstream(HOST, PORT, SYS_DEFAULT_NAMESPACE_ID); + long afterTime = System.currentTimeMillis(); + + Assert.assertNotNull(result); + Assert.assertTrue(result.getTimestamp() >= beforeTime); + Assert.assertTrue(result.getTimestamp() <= afterTime); + } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java index 92c9302bbf94..a70870ae337d 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/HttpUtilsTest.java @@ -18,6 +18,16 @@ package org.apache.shenyu.admin.utils; import com.sun.net.httpserver.HttpServer; +import java.io.ByteArrayInputStream; +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import okhttp3.FormBody; import okhttp3.HttpUrl; import okhttp3.Request; @@ -26,15 +36,8 @@ import org.junit.Test; import org.junit.jupiter.api.Assertions; -import java.net.InetSocketAddress; -import java.io.File; -import java.io.IOException; -import java.nio.charset.StandardCharsets; -import java.util.HashMap; -import java.util.Map; - /** - * HTTP request tool test {@link HttpUtils}. + * Test case for {@link HttpUtils}. */ public class HttpUtilsTest { @@ -191,4 +194,253 @@ public void requestForResponseShouldFollowRedirectsByDefault() throws IOExceptio targetServer.stop(0); } } + + @Test + public void constructorWithHttpToolConfigTest() { + HttpUtils.HttpToolConfig config = new HttpUtils.HttpToolConfig(); + config.setConnectTimeoutSeconds(5); + config.setReadTimeoutSeconds(5); + config.setWriteTimeoutSeconds(5); + HttpUtils httpUtils = new HttpUtils(config); + Assert.assertNotNull(httpUtils); + } + + @Test + public void httpToolConfigGettersAndSettersTest() { + HttpUtils.HttpToolConfig config = new HttpUtils.HttpToolConfig(); + config.setConnectTimeoutSeconds(10); + config.setReadTimeoutSeconds(15); + config.setWriteTimeoutSeconds(20); + Assert.assertEquals(10, config.getConnectTimeoutSeconds()); + Assert.assertEquals(15, config.getReadTimeoutSeconds()); + Assert.assertEquals(20, config.getWriteTimeoutSeconds()); + } + + @Test + public void httpMethodFromValueTest() { + Assert.assertEquals(HttpUtils.HTTPMethod.GET, HttpUtils.HTTPMethod.fromValue("GET")); + Assert.assertEquals(HttpUtils.HTTPMethod.POST, HttpUtils.HTTPMethod.fromValue("post")); + Assert.assertEquals(HttpUtils.HTTPMethod.PUT, HttpUtils.HTTPMethod.fromValue("Put")); + } + + @Test + public void httpMethodValueTest() { + Assert.assertEquals("GET", HttpUtils.HTTPMethod.GET.value()); + Assert.assertEquals("POST", HttpUtils.HTTPMethod.POST.value()); + Assert.assertEquals("PUT", HttpUtils.HTTPMethod.PUT.value()); + Assert.assertEquals("DELETE", HttpUtils.HTTPMethod.DELETE.value()); + Assert.assertEquals("HEAD", HttpUtils.HTTPMethod.HEAD.value()); + } + + @Test + public void uploadFileConstructorWithFileTest() throws IOException { + File tempFile = File.createTempFile("test", ".txt"); + tempFile.deleteOnExit(); + HttpUtils.UploadFile uploadFile = new HttpUtils.UploadFile("test", tempFile); + Assert.assertNotNull(uploadFile); + Assert.assertEquals("test", uploadFile.getName()); + Assert.assertEquals(tempFile.getName(), uploadFile.getFileName()); + Assert.assertNotNull(uploadFile.getFileData()); + } + + @Test + public void uploadFileConstructorWithInputStreamTest() throws IOException { + String testData = "test data"; + ByteArrayInputStream inputStream = new ByteArrayInputStream(testData.getBytes()); + HttpUtils.UploadFile uploadFile = new HttpUtils.UploadFile("test", "test.txt", inputStream); + Assert.assertNotNull(uploadFile); + Assert.assertEquals("test", uploadFile.getName()); + Assert.assertEquals("test.txt", uploadFile.getFileName()); + Assert.assertArrayEquals(testData.getBytes(), uploadFile.getFileData()); + } + + @Test + public void uploadFileConstructorWithByteArrayTest() { + byte[] data = "test data".getBytes(); + HttpUtils.UploadFile uploadFile = new HttpUtils.UploadFile("test", "test.txt", data); + Assert.assertNotNull(uploadFile); + Assert.assertEquals("test", uploadFile.getName()); + Assert.assertEquals("test.txt", uploadFile.getFileName()); + Assert.assertArrayEquals(data, uploadFile.getFileData()); + Assert.assertNotNull(uploadFile.getMd5()); + } + + @Test + public void uploadFileGettersAndSettersTest() { + HttpUtils.UploadFile uploadFile = new HttpUtils.UploadFile("test", "test.txt", "data".getBytes()); + uploadFile.setName("newName"); + uploadFile.setFileName("newFile.txt"); + uploadFile.setFileData("newData".getBytes()); + uploadFile.setMd5("newMd5"); + Assert.assertEquals("newName", uploadFile.getName()); + Assert.assertEquals("newFile.txt", uploadFile.getFileName()); + Assert.assertArrayEquals("newData".getBytes(), uploadFile.getFileData()); + Assert.assertEquals("newMd5", uploadFile.getMd5()); + } + + @Test + public void fileUtilsToBytesByInputStreamTest() throws IOException { + String testData = "test data"; + ByteArrayInputStream inputStream = new ByteArrayInputStream(testData.getBytes()); + byte[] result = HttpUtils.FileUtils.toBytes(inputStream); + Assert.assertArrayEquals(testData.getBytes(), result); + } + + @Test + public void getRequestTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "Hello World"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + try { + String result = httpUtils.get(url, new HashMap<>()); + Assert.assertEquals("Hello World", result); + } finally { + server.stop(0); + } + } + + @Test + public void requestMethodTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "POST response"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + try { + String result = httpUtils.request(url, new HashMap<>(), new HashMap<>(), HttpUtils.HTTPMethod.POST); + Assert.assertEquals("POST response", result); + } finally { + server.stop(0); + } + } + + @Test + public void requestJsonMethodTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "{\"status\":\"ok\"}"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + Map headers = new HashMap<>(); + headers.put("Content-Type", "application/json"); + try { + Response response = httpUtils.requestJson(url, "{\"test\":\"data\"}", headers, HttpUtils.HTTPMethod.POST); + Assert.assertEquals(200, response.code()); + } finally { + server.stop(0); + } + } + + @Test + public void requestFileStringMethodTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "File uploaded"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + List files = new ArrayList<>(); + files.add(new HttpUtils.UploadFile("file", "test.txt", "content".getBytes())); + try { + String result = httpUtils.requestFileString(url, new HashMap<>(), new HashMap<>(), files); + Assert.assertEquals("File uploaded", result); + } finally { + server.stop(0); + } + } + + @Test + public void requestFileMethodTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "File uploaded"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + List files = new ArrayList<>(); + files.add(new HttpUtils.UploadFile("file", "test.txt", "content".getBytes())); + try { + Response response = httpUtils.requestFile(url, new HashMap<>(), new HashMap<>(), files); + Assert.assertEquals(200, response.code()); + } finally { + server.stop(0); + } + } + + @Test + public void requestCallMethodTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "Call response"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + List files = new ArrayList<>(); + files.add(new HttpUtils.UploadFile("file", "test.txt", "content".getBytes())); + try { + Response response = httpUtils.requestCall(url, new HashMap<>(), new HashMap<>(), HttpUtils.HTTPMethod.POST, files); + Assert.assertEquals(200, response.code()); + } finally { + server.stop(0); + } + } + + @Test + public void downloadFileMethodTest() throws IOException { + HttpServer server = HttpServer.create(new InetSocketAddress(0), 0); + server.createContext("/test", exchange -> { + String response = "File content"; + exchange.sendResponseHeaders(200, response.length()); + exchange.getResponseBody().write(response.getBytes()); + exchange.close(); + }); + server.start(); + + HttpUtils httpUtils = new HttpUtils(); + String url = LOCALHOST_BASE_URL + server.getAddress().getPort() + "/test"; + try { + InputStream inputStream = httpUtils.downloadFile(url, new HashMap<>(), new HashMap<>()); + Assert.assertNotNull(inputStream); + // Note: Due to the current implementation closing the response immediately, + // we can only verify the stream is returned, not read its content + // This is a limitation of the current downloadFile implementation + } finally { + server.stop(0); + } + } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/JwtUtilsTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/JwtUtilsTest.java index 5527805b6484..5611e2532d12 100644 --- a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/JwtUtilsTest.java +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/JwtUtilsTest.java @@ -18,19 +18,25 @@ package org.apache.shenyu.admin.utils; import org.apache.shenyu.admin.config.properties.JwtProperties; +import org.apache.shenyu.admin.model.custom.UserInfo; import org.apache.shenyu.admin.spring.SpringBeanUtils; +import org.apache.shiro.subject.Subject; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; import org.springframework.context.ConfigurableApplicationContext; import static org.hamcrest.MatcherAssert.assertThat; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.notNullValue; +import static org.hamcrest.Matchers.nullValue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.assertThrows; /** - * test case for {@link JwtUtils}. + * Test case for {@link JwtUtils}. */ public class JwtUtilsTest { @@ -66,4 +72,150 @@ public void testGenerateToken() { assertThat(JwtUtils.getIssuer(token), is("userName")); assertThat(JwtUtils.getClientId(token), is("clientId")); } + + @Test + public void testGenerateTokenWithExpireSeconds() { + Long expireSeconds = 3600L; + // 1 hour + String token = JwtUtils.generateToken("userName", KEY, "clientId", expireSeconds); + assertThat(token, notNullValue()); + assertThat(JwtUtils.getIssuer(token), is("userName")); + assertThat(JwtUtils.getClientId(token), is("clientId")); + // Verify token is valid + assertThat(JwtUtils.verifyToken(token, KEY), is(true)); + } + + @Test + public void testGenerateTokenWithNullExpireSeconds() { + String token = JwtUtils.generateToken("userName", KEY, "clientId", null); + assertThat(token, notNullValue()); + assertThat(JwtUtils.getIssuer(token), is("userName")); + assertThat(JwtUtils.getClientId(token), is("clientId")); + } + + @Test + public void testGenerateTokenWithNullUserName() { + String token = JwtUtils.generateToken(null, KEY, "clientId"); + assertThat(token, notNullValue()); + assertThat(JwtUtils.getIssuer(token), is("")); + assertThat(JwtUtils.getClientId(token), is("clientId")); + } + + @Test + public void testGenerateTokenWithNullClientId() { + String token = JwtUtils.generateToken("userName", KEY, null); + assertThat(token, notNullValue()); + assertThat(JwtUtils.getIssuer(token), is("userName")); + assertThat(JwtUtils.getClientId(token), is("")); + } + + @Test + public void testGenerateTokenWithEmptyKey() { + String token = JwtUtils.generateToken("userName", "", "clientId"); + assertThat(token, is("")); + } + + @Test + public void testGenerateTokenWithNullKey() { + String token = JwtUtils.generateToken("userName", null, "clientId"); + assertThat(token, is("")); + } + + @Test + public void testVerifyTokenWithValidToken() { + String token = JwtUtils.generateToken("userName", KEY, "clientId"); + assertThat(JwtUtils.verifyToken(token, KEY), is(true)); + } + + @Test + public void testVerifyTokenWithInvalidToken() { + assertThat(JwtUtils.verifyToken("invalid.token.here", KEY), is(false)); + } + + @Test + public void testVerifyTokenWithWrongKey() { + String token = JwtUtils.generateToken("userName", KEY, "clientId"); + assertThat(JwtUtils.verifyToken(token, "wrong-key"), is(false)); + } + + @Test + public void testVerifyTokenWithNullToken() { + assertThrows(NullPointerException.class, () -> JwtUtils.verifyToken(null, KEY)); + } + + @Test + public void testVerifyTokenWithNullKey() { + String token = JwtUtils.generateToken("userName", KEY, "clientId"); + assertThrows(IllegalArgumentException.class, () -> JwtUtils.verifyToken(token, null)); + } + + @Test + public void testVerifyTokenWithEmptyKey() { + String token = JwtUtils.generateToken("userName", KEY, "clientId"); + assertThrows(IllegalArgumentException.class, () -> JwtUtils.verifyToken(token, "")); + } + + @Test + public void testVerifyTokenWithExpiredToken() { + // Generate token with negative expiration to get an already-expired token + String token = JwtUtils.generateToken("userName", KEY, "clientId", -1000L); + assertThat(JwtUtils.verifyToken(token, KEY), is(false)); + } + + @Test + public void testGetIssuerWithNullToken() { + assertThrows(NullPointerException.class, () -> JwtUtils.getIssuer(null)); + } + + @Test + public void testGetIssuerWithEmptyToken() { + assertThrows(Exception.class, () -> JwtUtils.getIssuer("")); + } + + @Test + public void testGetIssuerWithInvalidToken() { + assertThrows(Exception.class, () -> JwtUtils.getIssuer("invalid.token")); + } + + @Test + public void testGetClientIdWithNullToken() { + assertThrows(NullPointerException.class, () -> JwtUtils.getClientId(null)); + } + + @Test + public void testGetClientIdWithEmptyToken() { + assertThrows(Exception.class, () -> JwtUtils.getClientId("")); + } + + @Test + public void testGetClientIdWithInvalidToken() { + assertThrows(Exception.class, () -> JwtUtils.getClientId("invalid.token")); + } + + @Test + public void testGetUserInfo() { + UserInfo mockUserInfo = mock(UserInfo.class); + Subject mockSubject = mock(Subject.class); + when(mockSubject.getPrincipal()).thenReturn(mockUserInfo); + + try (MockedStatic securityUtilsMocked = mockStatic(org.apache.shiro.SecurityUtils.class)) { + securityUtilsMocked.when(org.apache.shiro.SecurityUtils::getSubject).thenReturn(mockSubject); + + UserInfo result = JwtUtils.getUserInfo(); + assertThat(result, is(mockUserInfo)); + } + } + + @Test + public void testGetUserInfoWithNullPrincipal() { + Subject mockSubject = mock(Subject.class); + when(mockSubject.getPrincipal()).thenReturn(null); + + try (MockedStatic securityUtilsMocked = mockStatic(org.apache.shiro.SecurityUtils.class)) { + securityUtilsMocked.when(org.apache.shiro.SecurityUtils::getSubject).thenReturn(mockSubject); + + UserInfo result = JwtUtils.getUserInfo(); + assertThat(result, nullValue()); + } + } } diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/ResourceUtilTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/ResourceUtilTest.java new file mode 100644 index 000000000000..ef24f30fcf81 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/ResourceUtilTest.java @@ -0,0 +1,332 @@ +/* + * 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 org.apache.shenyu.admin.utils; + +import org.apache.shenyu.admin.model.entity.ResourceDO; +import org.apache.shenyu.admin.model.vo.ResourceVO; +import org.apache.shenyu.common.constant.AdminConstants; +import org.apache.shenyu.common.enums.AdminPluginOperateEnum; +import org.apache.shenyu.common.enums.AdminResourceEnum; +import org.apache.shenyu.common.enums.ConfigGroupEnum; +import org.junit.Assert; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Test case for {@link ResourceUtil}. + */ +public class ResourceUtilTest { + + @Test + public void testBuildPluginResource() { + String pluginName = "test-plugin"; + ResourceDO resource = ResourceUtil.buildPluginResource(pluginName); + + Assert.assertNotNull(resource); + Assert.assertEquals(AdminConstants.RESOURCE_PLUGIN_ID, resource.getParentId()); + Assert.assertEquals(pluginName, resource.getTitle()); + Assert.assertEquals(pluginName, resource.getName()); + Assert.assertEquals(AdminConstants.RESOURCE_PLUGIN_URL_PREFIX + pluginName, resource.getUrl()); + Assert.assertEquals(pluginName, resource.getComponent()); + Assert.assertEquals(AdminResourceEnum.SECOND_MENU.getCode(), resource.getResourceType().intValue()); + Assert.assertEquals(AdminConstants.RESOURCE_PLUGIN_DEFAULT_ICON, resource.getIcon()); + Assert.assertFalse(resource.getIsLeaf()); + Assert.assertEquals(Integer.valueOf(1), resource.getStatus()); + } + + @Test + public void testBuildPluginDataPermissionResource() { + String pluginResourceId = "test-plugin-id"; + String pluginName = "test-plugin"; + + List resources = ResourceUtil.buildPluginDataPermissionResource(pluginResourceId, pluginName); + + Assert.assertNotNull(resources); + Assert.assertEquals(9, resources.size()); + // 3 selector + 3 rule + 1 plugin sync + + // Verify selector permissions + long selectorCount = resources.stream() + .filter(r -> r.getPerms().contains("Selector")) + .count(); + Assert.assertEquals(4, selectorCount); + // add, delete, edit, query + + // Verify rule permissions + long ruleCount = resources.stream() + .filter(r -> r.getPerms().contains("Rule")) + .count(); + Assert.assertEquals(4, ruleCount); + // add, delete, edit, query + + // Verify plugin sync permission + long syncCount = resources.stream() + .filter(r -> r.getPerms().contains("plugin:test-plugin:modify")) + .count(); + Assert.assertEquals(1, syncCount); + + // Verify all resources have correct parent ID + resources.forEach(r -> Assert.assertEquals(pluginResourceId, r.getParentId())); + } + + @Test + public void testBuildMenuWithEmptyList() { + List emptyList = Collections.emptyList(); + var result = ResourceUtil.buildMenu(emptyList); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testBuildMenuWithNullResources() { + List listWithNulls = new ArrayList<>(); + listWithNulls.add(null); + listWithNulls.add(createResourceVO("test", null, "Test Resource")); + var result = ResourceUtil.buildMenu(listWithNulls); + Assert.assertNotNull(result); + // Should handle nulls gracefully + } + + @Test + public void testBuildMenuWithNormalResources() { + List resources = new ArrayList<>(); + + // Root menu + ResourceVO root = createResourceVO("root", null, "Root Menu"); + resources.add(root); + + // Child menu + ResourceVO child = createResourceVO("child", "root", "Child Menu"); + resources.add(child); + + var result = ResourceUtil.buildMenu(resources); + Assert.assertNotNull(result); + Assert.assertFalse(result.isEmpty()); + + // Root should be in result + var rootMenu = result.stream() + .filter(m -> "root".equals(m.getId())) + .findFirst(); + Assert.assertTrue(rootMenu.isPresent()); + Assert.assertEquals("Root Menu", rootMenu.get().getMeta().getTitle()); + + // Child should be in root's children + Assert.assertNotNull(rootMenu.get().getChildren()); + Assert.assertFalse(rootMenu.get().getChildren().isEmpty()); + var childMenu = rootMenu.get().getChildren().stream() + .filter(m -> "child".equals(m.getId())) + .findFirst(); + Assert.assertTrue(childMenu.isPresent()); + Assert.assertEquals("Child Menu", childMenu.get().getMeta().getTitle()); + } + + @Test + public void testGetDeleteResourceIdsWithEmptyInputs() { + List emptyIds = Collections.emptyList(); + List emptyResources = Collections.emptyList(); + + var result = ResourceUtil.getDeleteResourceIds(emptyIds, emptyResources); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetDeleteResourceIdsWithNullInputs() { + var result = ResourceUtil.getDeleteResourceIds(null, null); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testGetDeleteResourceIdsWithSingleResource() { + List resourceIds = List.of("test-id"); + List allResources = List.of( + createResourceDO("test-id", "Test Resource") + ); + + var result = ResourceUtil.getDeleteResourceIds(resourceIds, allResources); + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("test-id", result.get(0).getId()); + } + + @Test + public void testGetDeleteResourceIdsWithParentChildRelationship() { + List resourceIds = List.of("parent-id"); + + ResourceDO parent = createResourceDO("parent-id", "Parent"); + ResourceDO child = createResourceDO("child-id", "Child"); + child.setParentId("parent-id"); + + List allResources = List.of(parent, child); + + var result = ResourceUtil.getDeleteResourceIds(resourceIds, allResources); + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + // Should include both parent and child + + // Check that both IDs are present + var ids = result.stream().map(ResourceDO::getId).toList(); + Assert.assertTrue(ids.contains("parent-id")); + Assert.assertTrue(ids.contains("child-id")); + } + + @Test + public void testGetDeleteResourceIdsWithMultipleLevels() { + // parent -> child -> grandchild + List resourceIds = List.of("parent-id"); + + ResourceDO parent = createResourceDO("parent-id", "Parent"); + ResourceDO child = createResourceDO("child-id", "Child"); + child.setParentId("parent-id"); + ResourceDO grandchild = createResourceDO("grandchild-id", "Grandchild"); + grandchild.setParentId("child-id"); + + List allResources = List.of(parent, child, grandchild); + + var result = ResourceUtil.getDeleteResourceIds(resourceIds, allResources); + Assert.assertNotNull(result); + Assert.assertEquals(3, result.size()); + // Should include all three levels + + var ids = result.stream().map(ResourceDO::getId).toList(); + Assert.assertTrue(ids.contains("parent-id")); + Assert.assertTrue(ids.contains("child-id")); + Assert.assertTrue(ids.contains("grandchild-id")); + } + + @Test + public void testGetDeleteResourceIdsWithNonExistentId() { + List resourceIds = List.of("non-existent-id"); + List allResources = List.of( + createResourceDO("existing-id", "Existing") + ); + + var result = ResourceUtil.getDeleteResourceIds(resourceIds, allResources); + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + // Non-existent ID should not cause issues + } + + @Test + public void testDataPermissionConstructor() { + var dataPermission = new ResourceUtil.DataPermission(ConfigGroupEnum.SELECTOR, AdminPluginOperateEnum.ADD); + Assert.assertNotNull(dataPermission); + // Since fields are private, we can't directly test them, but constructor should work + } + + @Test + public void testBuildMenuSorting() { + // Create resources with different sort orders + ResourceVO parent = createResourceVO("parent", null, "Parent"); + parent.setSort(1); + + ResourceVO child1 = createResourceVO("child1", "parent", "Child 1"); + child1.setSort(2); + + ResourceVO child2 = createResourceVO("child2", "parent", "Child 2"); + child2.setSort(1); + // Should come before child1 + + List resources = new ArrayList<>(); + resources.add(parent); + resources.add(child1); + resources.add(child2); + + var result = ResourceUtil.buildMenu(resources); + + // Find parent and check children order + var parentMenu = result.stream() + .filter(m -> "parent".equals(m.getId())) + .findFirst(); + Assert.assertTrue(parentMenu.isPresent()); + + var children = parentMenu.get().getChildren(); + Assert.assertNotNull(children); + Assert.assertEquals(2, children.size()); + + // child2 should come before child1 due to sort order + Assert.assertEquals("child2", children.get(0).getId()); + Assert.assertEquals("child1", children.get(1).getId()); + } + + @Test + public void testBuildMenuWithNullSort() { + ResourceVO parent = createResourceVO("parent", null, "Parent"); + parent.setSort(null); + + ResourceVO child1 = createResourceVO("child1", "parent", "Child 1"); + child1.setSort(1); + + ResourceVO child2 = createResourceVO("child2", "parent", "Child 2"); + child2.setSort(null); + + List resources = new ArrayList<>(); + resources.add(parent); + resources.add(child1); + resources.add(child2); + + var result = ResourceUtil.buildMenu(resources); + + // Should handle null sort values gracefully + var parentMenu = result.stream() + .filter(m -> "parent".equals(m.getId())) + .findFirst(); + Assert.assertTrue(parentMenu.isPresent()); + Assert.assertNotNull(parentMenu.get().getChildren()); + } + + private ResourceVO createResourceVO(final String id, final String parentId, final String title) { + ResourceVO vo = new ResourceVO(); + vo.setId(id); + vo.setParentId(parentId); + vo.setTitle(title); + vo.setName(title); + vo.setUrl("/test"); + vo.setComponent("TestComponent"); + vo.setResourceType(1); + vo.setSort(1); + vo.setIcon("icon"); + vo.setIsLeaf(false); + vo.setIsRoute(0); + vo.setPerms("test:perm"); + vo.setStatus(1); + return vo; + } + + private ResourceDO createResourceDO(final String id, final String title) { + ResourceDO resourceDO = new ResourceDO(); + resourceDO.setId(id); + resourceDO.setTitle(title); + resourceDO.setName(title); + resourceDO.setUrl("/test"); + resourceDO.setComponent("TestComponent"); + resourceDO.setResourceType(1); + resourceDO.setSort(1); + resourceDO.setIcon("icon"); + resourceDO.setIsLeaf(false); + resourceDO.setIsRoute(0); + resourceDO.setPerms("test:perm"); + resourceDO.setStatus(1); + return resourceDO; + } +} + diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/SelectorUtilTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/SelectorUtilTest.java new file mode 100644 index 000000000000..511d62990d8d --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/SelectorUtilTest.java @@ -0,0 +1,321 @@ +/* + * 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 org.apache.shenyu.admin.utils; + +import org.apache.shenyu.admin.model.dto.SelectorConditionDTO; +import org.apache.shenyu.admin.model.dto.SelectorDTO; +import org.apache.shenyu.admin.model.entity.SelectorDO; +import org.apache.shenyu.common.constant.Constants; +import org.apache.shenyu.common.dto.convert.selector.DivideUpstream; +import org.apache.shenyu.common.enums.MatchModeEnum; +import org.apache.shenyu.common.enums.OperatorEnum; +import org.apache.shenyu.common.enums.ParamTypeEnum; +import org.apache.shenyu.common.enums.PluginEnum; +import org.apache.shenyu.common.enums.SelectorTypeEnum; +import org.junit.Assert; +import org.junit.Test; + +import java.util.List; + +/** + * Test case for {@link SelectorUtil}. + */ +public class SelectorUtilTest { + + @Test + public void testBuildDivideUpstreamWithDividePluginAndValidHandle() { + // Test case: plugin is divide and handle is not blank - should parse JSON + SelectorDO selectorDO = new SelectorDO(); + String jsonHandle = "[{\"upstreamHost\":\"localhost\",\"protocol\":\"http\",\"upstreamUrl\":\"http://localhost:8080\",\"weight\":50,\"warmup\":100}]"; + selectorDO.setHandle(jsonHandle); + + List result = SelectorUtil.buildDivideUpstream(selectorDO, PluginEnum.DIVIDE.getName()); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + Assert.assertEquals("localhost", result.get(0).getUpstreamHost()); + Assert.assertEquals("http", result.get(0).getProtocol()); + Assert.assertEquals("http://localhost:8080", result.get(0).getUpstreamUrl()); + Assert.assertEquals(50, result.get(0).getWeight()); + Assert.assertEquals(100, result.get(0).getWarmup()); + } + + @Test + public void testBuildDivideUpstreamWithDividePluginAndEmptyHandle() { + // Test case: plugin is divide but handle is empty - should return empty list + SelectorDO selectorDO = new SelectorDO(); + selectorDO.setHandle(""); + + List result = SelectorUtil.buildDivideUpstream(selectorDO, PluginEnum.DIVIDE.getName()); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testBuildDivideUpstreamWithDividePluginAndNullHandle() { + // Test case: plugin is divide but handle is null - should return empty list + SelectorDO selectorDO = new SelectorDO(); + selectorDO.setHandle(null); + + List result = SelectorUtil.buildDivideUpstream(selectorDO, PluginEnum.DIVIDE.getName()); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testBuildDivideUpstreamWithNonDividePlugin() { + // Test case: plugin is not divide - should return empty list + SelectorDO selectorDO = new SelectorDO(); + selectorDO.setHandle("[{\"upstreamHost\":\"localhost\"}]"); + + List result = SelectorUtil.buildDivideUpstream(selectorDO, "other-plugin"); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test(expected = NullPointerException.class) + public void testBuildDivideUpstreamWithNullSelectorDO() { + // Test case: selectorDO is null - should throw NullPointerException + SelectorUtil.buildDivideUpstream(null, PluginEnum.DIVIDE.getName()); + } + + @Test + public void testBuildDivideUpstreamWithNullPluginName() { + // Test case: pluginName is null - should return empty list + SelectorDO selectorDO = new SelectorDO(); + selectorDO.setHandle("[{\"upstreamHost\":\"localhost\"}]"); + + List result = SelectorUtil.buildDivideUpstream(selectorDO, null); + + Assert.assertNotNull(result); + Assert.assertTrue(result.isEmpty()); + } + + @Test + public void testBuildDivideUpstreamWithMultipleUpstreams() { + // Test case: multiple upstreams in JSON + SelectorDO selectorDO = new SelectorDO(); + String jsonHandle = "[{\"upstreamHost\":\"host1\",\"protocol\":\"http\",\"upstreamUrl\":\"http://host1:8080\",\"weight\":30,\"warmup\":50}," + + "{\"upstreamHost\":\"host2\",\"protocol\":\"http\",\"upstreamUrl\":\"http://host2:8080\",\"weight\":70,\"warmup\":75}]"; + selectorDO.setHandle(jsonHandle); + + List result = SelectorUtil.buildDivideUpstream(selectorDO, PluginEnum.DIVIDE.getName()); + + Assert.assertNotNull(result); + Assert.assertEquals(2, result.size()); + Assert.assertEquals("host1", result.get(0).getUpstreamHost()); + Assert.assertEquals("host2", result.get(1).getUpstreamHost()); + } + + @Test + public void testBuildSelectorDTO() { + // Test normal case + String contextPath = "/test"; + String pluginId = "test-plugin-id"; + + SelectorDTO result = SelectorUtil.buildSelectorDTO(contextPath, pluginId); + + Assert.assertNotNull(result); + Assert.assertEquals(contextPath, result.getName()); + Assert.assertEquals(pluginId, result.getPluginId()); + Assert.assertEquals(Long.valueOf(SelectorTypeEnum.CUSTOM_FLOW.getCode()), Long.valueOf(result.getType())); + Assert.assertEquals(Long.valueOf(MatchModeEnum.AND.getCode()), Long.valueOf(result.getMatchMode())); + Assert.assertTrue(result.getEnabled()); + Assert.assertTrue(result.getLoged()); + Assert.assertTrue(result.getContinued()); + Assert.assertFalse(result.getMatchRestful()); + Assert.assertEquals(Long.valueOf(1), Long.valueOf(result.getSort())); + + // Check selector conditions + Assert.assertNotNull(result.getSelectorConditions()); + Assert.assertEquals(1, result.getSelectorConditions().size()); + SelectorConditionDTO condition = result.getSelectorConditions().get(0); + Assert.assertEquals(ParamTypeEnum.URI.getName(), condition.getParamType()); + Assert.assertEquals("/", condition.getParamName()); + Assert.assertEquals(OperatorEnum.STARTS_WITH.getAlias(), condition.getOperator()); + Assert.assertEquals(contextPath + Constants.PATH_SEPARATOR, condition.getParamValue()); + } + + @Test + public void testBuildSelectorDTOWithNullInputs() { + // Test with null inputs + SelectorDTO result = SelectorUtil.buildSelectorDTO(null, null); + + Assert.assertNotNull(result); + Assert.assertNull(result.getName()); + Assert.assertNull(result.getPluginId()); + Assert.assertNotNull(result.getSelectorConditions()); + } + + @Test + public void testBuildSelectorDTOWithEmptyInputs() { + // Test with empty inputs + SelectorDTO result = SelectorUtil.buildSelectorDTO("", ""); + + Assert.assertNotNull(result); + Assert.assertEquals("", result.getName()); + Assert.assertEquals("", result.getPluginId()); + Assert.assertNotNull(result.getSelectorConditions()); + Assert.assertEquals(1, result.getSelectorConditions().size()); + Assert.assertEquals(Constants.PATH_SEPARATOR, result.getSelectorConditions().get(0).getParamValue()); + } + + @Test + public void testBuildDefaultSelectorDTO() { + // Test normal case + String name = "test-selector"; + + SelectorDTO result = SelectorUtil.buildDefaultSelectorDTO(name); + + Assert.assertNotNull(result); + Assert.assertEquals(name, result.getName()); + Assert.assertEquals(Long.valueOf(SelectorTypeEnum.CUSTOM_FLOW.getCode()), Long.valueOf(result.getType())); + Assert.assertEquals(Long.valueOf(MatchModeEnum.AND.getCode()), Long.valueOf(result.getMatchMode())); + Assert.assertTrue(result.getEnabled()); + Assert.assertTrue(result.getLoged()); + Assert.assertTrue(result.getContinued()); + Assert.assertFalse(result.getMatchRestful()); + Assert.assertEquals(Long.valueOf(1), Long.valueOf(result.getSort())); + } + + @Test + public void testBuildDefaultSelectorDTOWithNullName() { + // Test with null name + SelectorDTO result = SelectorUtil.buildDefaultSelectorDTO(null); + + Assert.assertNotNull(result); + Assert.assertNull(result.getName()); + Assert.assertEquals(Long.valueOf(SelectorTypeEnum.CUSTOM_FLOW.getCode()), Long.valueOf(result.getType())); + Assert.assertEquals(Long.valueOf(MatchModeEnum.AND.getCode()), Long.valueOf(result.getMatchMode())); + Assert.assertTrue(result.getEnabled()); + Assert.assertTrue(result.getLoged()); + Assert.assertTrue(result.getContinued()); + Assert.assertFalse(result.getMatchRestful()); + Assert.assertEquals(Long.valueOf(1), Long.valueOf(result.getSort())); + } + + @Test + public void testBuildDefaultSelectorDTOWithEmptyName() { + // Test with empty name + SelectorDTO result = SelectorUtil.buildDefaultSelectorDTO(""); + + Assert.assertNotNull(result); + Assert.assertEquals("", result.getName()); + Assert.assertEquals(Long.valueOf(SelectorTypeEnum.CUSTOM_FLOW.getCode()), Long.valueOf(result.getType())); + Assert.assertEquals(Long.valueOf(MatchModeEnum.AND.getCode()), Long.valueOf(result.getMatchMode())); + Assert.assertTrue(result.getEnabled()); + Assert.assertTrue(result.getLoged()); + Assert.assertTrue(result.getContinued()); + Assert.assertFalse(result.getMatchRestful()); + Assert.assertEquals(Long.valueOf(1), Long.valueOf(result.getSort())); + } + + @Test + public void testBuildDefaultSelectorConditionDTO() { + // Test normal case + String contextPath = "/test"; + + List result = SelectorUtil.buildDefaultSelectorConditionDTO(contextPath); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + SelectorConditionDTO condition = result.get(0); + Assert.assertEquals(ParamTypeEnum.URI.getName(), condition.getParamType()); + Assert.assertEquals("/", condition.getParamName()); + Assert.assertEquals(OperatorEnum.STARTS_WITH.getAlias(), condition.getOperator()); + Assert.assertEquals(contextPath + Constants.PATH_SEPARATOR, condition.getParamValue()); + } + + @Test + public void testBuildDefaultSelectorConditionDTOWithNullContextPath() { + // Test with null context path - will concatenate with PATH_SEPARATOR + List result = SelectorUtil.buildDefaultSelectorConditionDTO(null); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + SelectorConditionDTO condition = result.get(0); + Assert.assertEquals(ParamTypeEnum.URI.getName(), condition.getParamType()); + Assert.assertEquals("/", condition.getParamName()); + Assert.assertEquals(OperatorEnum.STARTS_WITH.getAlias(), condition.getOperator()); + // null + PATH_SEPARATOR results in "null/" + Assert.assertEquals("null" + Constants.PATH_SEPARATOR, condition.getParamValue()); + } + + @Test + public void testBuildDefaultSelectorConditionDTOWithEmptyContextPath() { + // Test with empty context path + List result = SelectorUtil.buildDefaultSelectorConditionDTO(""); + + Assert.assertNotNull(result); + Assert.assertEquals(1, result.size()); + SelectorConditionDTO condition = result.get(0); + Assert.assertEquals(ParamTypeEnum.URI.getName(), condition.getParamType()); + Assert.assertEquals("/", condition.getParamName()); + Assert.assertEquals(OperatorEnum.STARTS_WITH.getAlias(), condition.getOperator()); + Assert.assertEquals(Constants.PATH_SEPARATOR, condition.getParamValue()); + } + + @Test + public void testBuildDivideUpstreamWithInvalidJson() { + // Test case: plugin is divide but handle contains invalid JSON - should throw exception + SelectorDO selectorDO = new SelectorDO(); + selectorDO.setHandle("invalid json"); + + Assert.assertThrows(RuntimeException.class, + () -> SelectorUtil.buildDivideUpstream(selectorDO, PluginEnum.DIVIDE.getName())); + } +} + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/WebI18nAssertTest.java b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/WebI18nAssertTest.java new file mode 100644 index 000000000000..b9be3d5bc351 --- /dev/null +++ b/shenyu-admin/src/test/java/org/apache/shenyu/admin/utils/WebI18nAssertTest.java @@ -0,0 +1,192 @@ +/* + * 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 org.apache.shenyu.admin.utils; + +import org.apache.shenyu.admin.exception.WebI18nException; +import org.junit.jupiter.api.Test; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertThrows; + +/** + * Test case for {@link WebI18nAssert}. + */ +public class WebI18nAssertTest { + + @Test + void testNotNullWithValidObject() { + assertDoesNotThrow(() -> { + WebI18nAssert.notNull("test", "error message"); + WebI18nAssert.notNull(123, "error message"); + WebI18nAssert.notNull(new Object(), "error message"); + }); + } + + @Test + void testNotNullWithNullObject() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notNull(null, "error message")); + } + + @Test + void testNotNullWithNullObjectAndParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notNull(null, "error message with param: %s", "param1")); + } + + @Test + void testIsNullWithNullObject() { + assertDoesNotThrow(() -> WebI18nAssert.isNull(null, "error message")); + } + + @Test + void testIsNullWithValidObject() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.isNull("test", "error message")); + } + + @Test + void testIsNullWithValidObjectAndParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.isNull(123, "error message with param: %s", "param1")); + } + + @Test + void testNotBlackWithValidString() { + assertDoesNotThrow(() -> { + WebI18nAssert.notBlack("test", "error message"); + WebI18nAssert.notBlack(" a ", "error message"); + WebI18nAssert.notBlack("test string", "error message"); + }); + } + + @Test + void testNotBlackWithNullString() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notBlack(null, "error message")); + } + + @Test + void testNotBlackWithEmptyString() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notBlack("", "error message")); + } + + @Test + void testNotBlackWithBlankString() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notBlack(" ", "error message")); + } + + @Test + void testNotBlackWithBlankStringAndParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notBlack("", "error message with param: %s", "param1")); + } + + @Test + void testNotEmptyWithValidCollection() { + assertDoesNotThrow(() -> { + List list = Arrays.asList("item1", "item2"); + WebI18nAssert.notEmpty(list, "error message"); + + Collection collection = new ArrayList<>(); + collection.add(1); + WebI18nAssert.notEmpty(collection, "error message"); + }); + } + + @Test + void testNotEmptyWithNullCollection() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notEmpty(null, "error message")); + } + + @Test + void testNotEmptyWithEmptyCollection() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notEmpty(Collections.emptyList(), "error message")); + } + + @Test + void testNotEmptyWithEmptyCollectionAndParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.notEmpty(new ArrayList<>(), "error message with param: %s", "param1")); + } + + @Test + void testIsTrueWithTrueBoolean() { + assertDoesNotThrow(() -> { + WebI18nAssert.isTrue(true, "error message"); + WebI18nAssert.isTrue(Boolean.TRUE, "error message"); + }); + } + + @Test + void testIsTrueWithFalseBoolean() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.isTrue(false, "error message")); + } + + @Test + void testIsTrueWithNullBoolean() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.isTrue(null, "error message")); + } + + @Test + void testIsTrueWithFalseBooleanAndParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.isTrue(Boolean.FALSE, "error message with param: %s", "param1")); + } + + @Test + void testFail() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.fail("error message")); + } + + @Test + void testFailWithParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.fail("error message with param: %s", "param1")); + } + + @Test + void testFailWithMultipleParams() { + assertThrows(WebI18nException.class, () -> WebI18nAssert.fail("error message with params: %s, %s, %d", "param1", "param2", 123)); + } + + @Test + void testNotNullWithParams() { + assertDoesNotThrow(() -> WebI18nAssert.notNull("test", "error message with param: %s", "param1")); + } + + @Test + void testIsNullWithParams() { + assertDoesNotThrow(() -> WebI18nAssert.isNull(null, "error message with param: %s", "param1")); + } + + @Test + void testNotBlackWithParams() { + assertDoesNotThrow(() -> WebI18nAssert.notBlack("test", "error message with param: %s", "param1")); + } + + @Test + void testNotEmptyWithParams() { + assertDoesNotThrow(() -> { + final List list = Collections.singletonList("item"); + WebI18nAssert.notEmpty(list, "error message with param: %s", "param1"); + }); + } + + @Test + void testIsTrueWithParams() { + assertDoesNotThrow(() -> WebI18nAssert.isTrue(true, "error message with param: %s", "param1")); + } +}