diff --git a/CHANGELOG.md b/CHANGELOG.md index ccb77dc41..691bceb0b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,3 +7,4 @@ - [feat: support gateway context, feign eager-load support default value.](https://github.com/Tencent/spring-cloud-tencent/pull/1496) - [feat:use polaris-all for shading third-party dependencies.](https://github.com/Tencent/spring-cloud-tencent/pull/1498) - [feat:support default instance circuit breaker rule.](https://github.com/Tencent/spring-cloud-tencent/pull/1499) +- [fix: fix count circuit breaker in gateway & return 404 when context api does not match.](https://github.com/Tencent/spring-cloud-tencent/pull/1508) diff --git a/spring-cloud-tencent-examples/quickstart-example/quickstart-gateway-service/src/main/resources/application-context.yml b/spring-cloud-tencent-examples/quickstart-example/quickstart-gateway-service/src/main/resources/application-context.yml new file mode 100644 index 000000000..5b90a3e2d --- /dev/null +++ b/spring-cloud-tencent-examples/quickstart-example/quickstart-gateway-service/src/main/resources/application-context.yml @@ -0,0 +1,31 @@ +spring: + cloud: + tencent: + gateway: + groups: + group-scg2p: + predicate: + apiType: ms + context: /group1 + namespace: + key: null + position: PATH + service: + key: null + position: PATH + routes: + - host: null + metadata: {} + method: GET + namespace: default + path: /echo/{param} + service: provider-demo + routes: + group1: + filters: + - Context=group1 + order: -1 + predicates: + - Context=group1 + - Path=/group1/** + uri: lb://group1 diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfiguration.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfiguration.java index 98a7a3636..eb68c1659 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfiguration.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfiguration.java @@ -59,7 +59,7 @@ public class GatewayPluginAutoConfiguration { @Value("${spring.cloud.polaris.discovery.eager-load.enabled:#{'true'}}") private boolean commonEagerLoadEnabled; - @Value("${spring.cloud.polaris.discovery.eager-load.gateway.enabled:#{'true'}}") + @Value("${spring.cloud.polaris.discovery.eager-load.gateway.enabled:#{'false'}}") private boolean gatewayEagerLoadEnabled; @Bean diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilter.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilter.java index 538f0e459..4df83f5e0 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilter.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilter.java @@ -46,7 +46,7 @@ public class PolarisReactiveLoadBalancerClientFilter extends ReactiveLoadBalance @Override public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) { // restore context from exchange - MetadataContext metadataContext = (MetadataContext) exchange.getAttributes().get( + MetadataContext metadataContext = exchange.getAttribute( MetadataConstant.HeaderName.METADATA_CONTEXT); if (metadataContext != null) { MetadataContextHolder.set(metadataContext); diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessor.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessor.java index 3055984f2..3d75335b0 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessor.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessor.java @@ -26,6 +26,10 @@ import org.springframework.context.ApplicationContext; import org.springframework.core.Ordered; public class PolarisReactiveLoadBalancerClientFilterBeanPostProcessor implements BeanPostProcessor, Ordered { + /** + * Order of this bean post processor. + */ + public static final int ORDER = 0; private ApplicationContext applicationContext; @@ -46,6 +50,6 @@ public class PolarisReactiveLoadBalancerClientFilterBeanPostProcessor implements @Override public int getOrder() { - return 0; + return ORDER; } } diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilter.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilter.java index 261e45c04..6c82c0d2b 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilter.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilter.java @@ -32,6 +32,7 @@ import org.springframework.cloud.gateway.filter.GatewayFilter; import org.springframework.cloud.gateway.filter.GatewayFilterChain; import org.springframework.cloud.gateway.filter.RouteToRequestUrlFilter; import org.springframework.cloud.gateway.route.Route; +import org.springframework.cloud.gateway.support.NotFoundException; import org.springframework.core.Ordered; import org.springframework.http.server.reactive.ServerHttpRequest; import org.springframework.web.server.ServerWebExchange; @@ -39,6 +40,7 @@ import org.springframework.web.server.ServerWebExchange; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + public class ContextGatewayFilter implements GatewayFilter, Ordered { private static final Logger logger = LoggerFactory.getLogger(ContextGatewayFilter.class); @@ -69,13 +71,15 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered { String[] apis = rebuildExternalApi(request, request.getPath().value()); GroupContext.ContextRoute contextRoute = manager.getGroupPathRoute(config.getGroup(), apis[0]); if (contextRoute == null) { - throw new RuntimeException(String.format("Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], request.getPath())); + String msg = String.format("[externalFilter] Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], request.getPath()); + logger.warn(msg); + throw NotFoundException.create(true, msg); } updateRouteMetadata(exchange, contextRoute); URI requestUri = URI.create(contextRoute.getHost() + apis[1]); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUri); - // 调整为正确路径 + // to correct path ServerHttpRequest newRequest = request.mutate().path(apis[1]).build(); return chain.filter(exchange.mutate().request(newRequest).build()); } @@ -83,22 +87,24 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered { private Mono<Void> msFilter(ServerWebExchange exchange, GatewayFilterChain chain, GroupContext groupContext) { ServerHttpRequest request = exchange.getRequest(); String[] apis = rebuildMsApi(request, groupContext, request.getPath().value()); - // 判断 api 是否匹配 + // check api GroupContext.ContextRoute contextRoute = manager.getGroupPathRoute(config.getGroup(), apis[0]); if (contextRoute == null) { - throw new RuntimeException(String.format("Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], request.getPath())); + String msg = String.format("[msFilter] Can't find context route for group: %s, path: %s, origin path: %s", config.getGroup(), apis[0], request.getPath()); + logger.warn(msg); + throw NotFoundException.create(true, msg); } updateRouteMetadata(exchange, contextRoute); - MetadataContext metadataContext = (MetadataContext) exchange.getAttributes().get( + MetadataContext metadataContext = exchange.getAttribute( MetadataConstant.HeaderName.METADATA_CONTEXT); - - metadataContext.putFragmentContext(MetadataContext.FRAGMENT_APPLICATION_NONE, - MetadataConstant.POLARIS_TARGET_NAMESPACE, contextRoute.getNamespace()); - + if (metadataContext != null) { + metadataContext.putFragmentContext(MetadataContext.FRAGMENT_APPLICATION_NONE, + MetadataConstant.POLARIS_TARGET_NAMESPACE, contextRoute.getNamespace()); + } URI requestUri = URI.create("lb://" + contextRoute.getService() + apis[1]); exchange.getAttributes().put(GATEWAY_REQUEST_URL_ATTR, requestUri); - // 调整为正确路径 + // to correct path ServerHttpRequest newRequest = request.mutate().path(apis[1]).build(); return chain.filter(exchange.mutate().request(newRequest).build()); } @@ -106,7 +112,7 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered { /** * e.g. "/context/api/test" → [ "GET|/api/test", "/api/test"] */ - private String[] rebuildExternalApi(ServerHttpRequest request, String path) { + String[] rebuildExternalApi(ServerHttpRequest request, String path) { String[] pathSegments = path.split("/"); StringBuilder matchPath = new StringBuilder(); StringBuilder realPath = new StringBuilder(); @@ -127,7 +133,7 @@ public class ContextGatewayFilter implements GatewayFilter, Ordered { * returns an array of two strings, the first is the match path, the second is the real path. * e.g. "/context/namespace/svc/api/test" → [ "GET|/namespace/svc/api/test", "/api/test"] */ - private String[] rebuildMsApi(ServerHttpRequest request, GroupContext groupContext, String path) { + String[] rebuildMsApi(ServerHttpRequest request, GroupContext groupContext, String path) { String[] pathSegments = path.split("/"); StringBuilder matchPath = new StringBuilder(); int index = 2; diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactory.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactory.java index 2293bee6f..b82884df2 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactory.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactory.java @@ -53,6 +53,4 @@ public class ContextGatewayFilterFactory extends AbstractGatewayFilterFactory<Co this.group = group; } } - - } diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManager.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManager.java index ae491587e..1e7930891 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManager.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManager.java @@ -52,6 +52,10 @@ public class ContextGatewayPropertiesManager { return groupPathRouteMap; } + public Map<String, Map<String, GroupContext.ContextRoute>> getGroupWildcardPathRouteMap() { + return groupWildcardPathRouteMap; + } + public void setGroupRouteMap(Map<String, GroupContext> groups) { ConcurrentHashMap<String, Map<String, GroupContext.ContextRoute>> newGroupPathRouteMap = new ConcurrentHashMap<>(); @@ -63,7 +67,7 @@ public class ContextGatewayPropertiesManager { for (GroupContext.ContextRoute route : entry.getValue().getRoutes()) { String path = route.getPath(); // convert path parameter to group wildcard path - if (path.contains("{") && path.contains("}") || path.contains("*")) { + if (path.contains("{") && path.contains("}") || path.contains("*") || path.contains("?")) { newGroupWildcardPathRoute.put(buildPathKey(entry.getValue(), route), route); } else { diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GatewayConfigChangeListener.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GatewayConfigChangeListener.java index 50b299856..20d94bf51 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GatewayConfigChangeListener.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GatewayConfigChangeListener.java @@ -46,8 +46,10 @@ public class GatewayConfigChangeListener { public void onChangeTencentGatewayProperties(ConfigChangeEvent event) { Binder binder = Binder.get(environment); BindResult<ContextGatewayProperties> result = binder.bind(ContextGatewayProperties.PREFIX, ContextGatewayProperties.class); - manager.setGroupRouteMap(result.get().getGroups()); - this.publisher.publishEvent(new RefreshRoutesEvent(event)); + if (result.isBound()) { + manager.setGroupRouteMap(result.get().getGroups()); + this.publisher.publishEvent(new RefreshRoutesEvent(event)); + } } @PolarisConfigKVFileChangeListener(interestedKeyPrefixes = GatewayProperties.PREFIX) diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GroupContext.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GroupContext.java index 6394cffc4..fcc596272 100644 --- a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GroupContext.java +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/java/com/tencent/cloud/plugin/gateway/context/GroupContext.java @@ -24,8 +24,6 @@ public class GroupContext { private String comment; - private ApiType apiType; - private ContextPredicate predicate; private List<ContextRoute> routes; @@ -38,14 +36,6 @@ public class GroupContext { this.comment = comment; } - public ApiType getApiType() { - return apiType; - } - - public void setApiType(ApiType apiType) { - this.apiType = apiType; - } - public ContextPredicate getPredicate() { return predicate; } diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/resources/META-INF/additional-spring-configuration-metadata.json new file mode 100644 index 000000000..88fe79c1f --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -0,0 +1,18 @@ +{ + "properties": [ + { + "name": "spring.cloud.tencent.gateway.routes", + "type": "java.util.Map<java.lang.String, org.springframework.cloud.gateway.route.RouteDefinition>", + "description": "Route definitions map for gateway context", + "sourceType": "com.tencent.cloud.plugin.gateway.context.ContextGatewayProperties", + "defaultValue": {} + }, + { + "name": "spring.cloud.tencent.gateway.groups", + "type": "java.util.Map<java.lang.String, com.tencent.cloud.plugin.gateway.context.GroupContext>", + "description": "Group contexts configuration", + "sourceType": "com.tencent.cloud.plugin.gateway.context.ContextGatewayProperties", + "defaultValue": {} + } + ] +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfigurationTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfigurationTest.java new file mode 100644 index 000000000..647d1cb5b --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/GatewayPluginAutoConfigurationTest.java @@ -0,0 +1,143 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway; + +import com.tencent.cloud.plugin.gateway.context.ContextGatewayFilterFactory; +import com.tencent.cloud.plugin.gateway.context.ContextGatewayProperties; +import com.tencent.cloud.plugin.gateway.context.ContextGatewayPropertiesManager; +import com.tencent.cloud.plugin.gateway.context.ContextPropertiesRouteDefinitionLocator; +import com.tencent.cloud.plugin.gateway.context.ContextRoutePredicateFactory; +import com.tencent.cloud.plugin.gateway.context.GatewayConfigChangeListener; +import com.tencent.cloud.polaris.config.config.PolarisConfigProperties; +import com.tencent.cloud.polaris.config.listener.ConfigChangeEvent; +import com.tencent.cloud.polaris.context.config.PolarisContextAutoConfiguration; +import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient; +import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.context.ConfigurationPropertiesAutoConfiguration; +import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.gateway.config.GatewayAutoConfiguration; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link GatewayPluginAutoConfiguration}. + */ +class GatewayPluginAutoConfigurationTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of( + ConfigurationPropertiesAutoConfiguration.class, + PropertyPlaceholderAutoConfiguration.class, + PolarisContextAutoConfiguration.class, + GatewayAutoConfiguration.class, + GatewayPluginAutoConfiguration.class + )) + .withPropertyValues( + "spring.cloud.gateway.enabled=false", // not needed for this test + "spring.cloud.tencent.plugin.scg.enabled=true", + "spring.cloud.tencent.plugin.scg.context.enabled=true", + "spring.cloud.tencent.gateway.routes.test_group.uri=lb://test-group" + ) + .withUserConfiguration(MockPolarisClientsConfiguration.class); + + @Test + void shouldCreateBeansWhenConditionsMet() { + contextRunner.run(context -> { + // Verify core beans + assertThat(context).hasSingleBean(ContextGatewayFilterFactory.class); + assertThat(context).hasSingleBean(ContextPropertiesRouteDefinitionLocator.class); + assertThat(context).hasSingleBean(ContextRoutePredicateFactory.class); + assertThat(context).hasSingleBean(ContextGatewayPropertiesManager.class); + assertThat(context).hasSingleBean(GatewayRegistrationCustomizer.class); + assertThat(context).hasSingleBean(GatewayConfigChangeListener.class); + assertThat(context).hasSingleBean(PolarisReactiveLoadBalancerClientFilterBeanPostProcessor.class); + + + GatewayPluginAutoConfiguration.ContextPluginConfiguration pluginConfiguration = + context.getBean(GatewayPluginAutoConfiguration.ContextPluginConfiguration.class); + assertThat(pluginConfiguration).hasFieldOrPropertyWithValue("commonEagerLoadEnabled", true) + .hasFieldOrPropertyWithValue("gatewayEagerLoadEnabled", false); + + ContextGatewayProperties properties = context.getBean(ContextGatewayProperties.class); + properties.setRoutes(properties.getRoutes()); + properties.setGroups(properties.getGroups()); + properties.toString(); + + // test listener + GatewayConfigChangeListener listener = context.getBean(GatewayConfigChangeListener.class); + listener.onChangeTencentGatewayProperties(new ConfigChangeEvent(null, null)); + listener.onChangeGatewayConfigChangeListener(new ConfigChangeEvent(null, null)); + + }); + } + + @Test + void shouldEagerLoad() { + contextRunner + .withPropertyValues("spring.cloud.polaris.discovery.eager-load.gateway.enabled=true") + .run(context -> { + // Verify eager loading + GatewayPluginAutoConfiguration.ContextPluginConfiguration pluginConfiguration = context.getBean(GatewayPluginAutoConfiguration.ContextPluginConfiguration.class); + assertThat(pluginConfiguration).hasFieldOrPropertyWithValue("commonEagerLoadEnabled", true) + .hasFieldOrPropertyWithValue("gatewayEagerLoadEnabled", true); + }); + } + + @Test + void shouldNotCreateBeansWhenPluginDisabled() { + new ApplicationContextRunner() + .withConfiguration(AutoConfigurations.of(GatewayPluginAutoConfiguration.class)) + .withPropertyValues("spring.cloud.tencent.plugin.scg.enabled=false") + .run(context -> assertThat(context).doesNotHaveBean(ContextGatewayFilterFactory.class)); + } + + @Test + void shouldNotCreateContextBeansWhenContextPluginDisabled() { + contextRunner + .withPropertyValues("spring.cloud.tencent.plugin.scg.context.enabled=false") + .run(context -> { + assertThat(context).doesNotHaveBean(ContextGatewayFilterFactory.class); + assertThat(context).doesNotHaveBean(ContextPropertiesRouteDefinitionLocator.class); + }); + } + + @Configuration + static class MockPolarisClientsConfiguration { + @Bean + PolarisDiscoveryClient polarisDiscoveryClient() { + return mock(PolarisDiscoveryClient.class); + } + + @Bean + PolarisReactiveDiscoveryClient polarisReactiveDiscoveryClient() { + return mock(PolarisReactiveDiscoveryClient.class); + } + + @Bean + PolarisConfigProperties polarisConfigProperties() { + return new PolarisConfigProperties(); + } + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/GatewayRegistrationCustomizerTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/GatewayRegistrationCustomizerTest.java new file mode 100644 index 000000000..bd8d04477 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/GatewayRegistrationCustomizerTest.java @@ -0,0 +1,70 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway; + +import java.util.HashMap; +import java.util.Map; + +import com.tencent.cloud.polaris.registry.PolarisRegistration; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +@ExtendWith(MockitoExtension.class) +class GatewayRegistrationCustomizerTest { + + @Mock + private PolarisRegistration polarisRegistration; + + private final GatewayRegistrationCustomizer customizer = new GatewayRegistrationCustomizer(); + + @Test + void shouldAddServiceTypeMetadata() { + // Arrange + Map<String, String> metadata = new HashMap<>(); + when(polarisRegistration.getMetadata()).thenReturn(metadata); + + // Act + customizer.customize(polarisRegistration); + + // Assert + assertThat(metadata) + .containsEntry("internal-service-type", "spring-cloud-gateway"); + } + + @Test + void shouldNotOverrideExistingMetadata() { + // Arrange + Map<String, String> metadata = new HashMap<>(); + metadata.put("existing-key", "existing-value"); + metadata.put("internal-service-type", "existing-type"); + when(polarisRegistration.getMetadata()).thenReturn(metadata); + + // Act + customizer.customize(polarisRegistration); + + // Assert + assertThat(metadata) + .containsEntry("internal-service-type", "spring-cloud-gateway") + .containsEntry("existing-key", "existing-value"); + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessorTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessorTest.java new file mode 100644 index 000000000..2f274870a --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterBeanPostProcessorTest.java @@ -0,0 +1,94 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties; +import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.context.ApplicationContext; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * + * Test for ${@link PolarisReactiveLoadBalancerClientFilterBeanPostProcessor} + * + * @author Shedfree Wu + */ +public class PolarisReactiveLoadBalancerClientFilterBeanPostProcessorTest { + + @Mock + private ApplicationContext applicationContext; + + @Mock + private LoadBalancerClientFactory loadBalancerClientFactory; + + @Mock + private GatewayLoadBalancerProperties gatewayLoadBalancerProperties; + + private PolarisReactiveLoadBalancerClientFilterBeanPostProcessor processor; + + @BeforeEach + protected void setUp() { + MockitoAnnotations.openMocks(this); + processor = new PolarisReactiveLoadBalancerClientFilterBeanPostProcessor(applicationContext); + + when(applicationContext.getBean(GatewayLoadBalancerProperties.class)).thenReturn(gatewayLoadBalancerProperties); + when(applicationContext.getBean(LoadBalancerClientFactory.class)).thenReturn(loadBalancerClientFactory); + } + + @Test + void testGetOrder() { + int order = processor.getOrder(); + Assertions.assertEquals(PolarisReactiveLoadBalancerClientFilterBeanPostProcessor.ORDER, order); + } + + @Test + void testPostProcessBeforeInitializationWithReactiveLoadBalancerClientFilter() { + // Arrange + ReactiveLoadBalancerClientFilter originalInterceptor = mock(ReactiveLoadBalancerClientFilter.class); + String beanName = "testBean"; + + // Act + Object result = processor.postProcessAfterInitialization(originalInterceptor, beanName); + + // Assert + Assertions.assertInstanceOf(PolarisReactiveLoadBalancerClientFilter.class, result); + } + + @Test + void testPostProcessBeforeInitializationWithNonReactiveLoadBalancerClientFilter() { + // Arrange + Object originalBean = new Object(); + String beanName = "testBean"; + + // Act + Object result = processor.postProcessAfterInitialization(originalBean, beanName); + + // Assert + Assertions.assertSame(originalBean, result); + } + +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterTest.java new file mode 100644 index 000000000..abf932a07 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/PolarisReactiveLoadBalancerClientFilterTest.java @@ -0,0 +1,107 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway; + +import com.tencent.cloud.common.constant.MetadataConstant; +import com.tencent.cloud.common.metadata.MetadataContext; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import org.junit.jupiter.api.Assertions; +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 reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.config.GatewayLoadBalancerProperties; +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.filter.ReactiveLoadBalancerClientFilter; +import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link PolarisReactiveLoadBalancerClientFilter}. + */ +@ExtendWith(MockitoExtension.class) +class PolarisReactiveLoadBalancerClientFilterTest { + + @Mock + private LoadBalancerClientFactory clientFactory; + @Mock + private GatewayLoadBalancerProperties properties; + @Mock + private ReactiveLoadBalancerClientFilter originalFilter; + @Mock + private ServerWebExchange exchange; + @Mock + private GatewayFilterChain chain; + + private PolarisReactiveLoadBalancerClientFilter polarisFilter; + private final MetadataContext testContext = new MetadataContext(); + + @BeforeEach + void setUp() { + polarisFilter = new PolarisReactiveLoadBalancerClientFilter(clientFactory, properties, originalFilter); + MetadataContextHolder.remove(); + } + + @Test + void testGetOrderDelegatesToOriginalFilter() { + // Arrange + when(originalFilter.getOrder()).thenReturn(42); + + // Act + assertThat(polarisFilter.getOrder()).isEqualTo(42); + verify(originalFilter).getOrder(); + } + + @Test + void testFilterRestoresMetadataContext() { + // Arrange + when(exchange.getAttribute(MetadataConstant.HeaderName.METADATA_CONTEXT)) + .thenReturn(testContext); + when(originalFilter.filter(exchange, chain)) + .thenReturn(Mono.empty()); + MetadataContext before = MetadataContextHolder.get(); + Assertions.assertNotEquals(testContext, before); + // Act + polarisFilter.filter(exchange, chain); + MetadataContext after = MetadataContextHolder.get(); + Assertions.assertEquals(testContext, after); + } + + @Test + void testFilterWithoutMetadataContext() { + // Arrange + when(exchange.getAttribute(MetadataConstant.HeaderName.METADATA_CONTEXT)) + .thenReturn(null); + when(originalFilter.filter(exchange, chain)) + .thenReturn(Mono.empty()); + MetadataContext before = MetadataContextHolder.get(); + // Act + polarisFilter.filter(exchange, chain); + MetadataContext after = MetadataContextHolder.get(); + + // Assert + Assertions.assertEquals(before, after); + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactoryTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactoryTest.java new file mode 100644 index 000000000..ce0c0f36d --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterFactoryTest.java @@ -0,0 +1,98 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway.context; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import org.springframework.cloud.gateway.filter.GatewayFilter; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ContextGatewayFilterFactory}. + */ +class ContextGatewayFilterFactoryTest { + + @Mock + private ContextGatewayPropertiesManager mockManager; + + private ContextGatewayFilterFactory filterFactory; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + filterFactory = new ContextGatewayFilterFactory(mockManager); + } + + // Test filter factory creates filter with correct configuration + @Test + void shouldCreateFilterWithValidConfig() { + // Setup test config + ContextGatewayFilterFactory.Config config = new ContextGatewayFilterFactory.Config(); + config.setGroup("test-group"); + + // Create filter instance + GatewayFilter filter = filterFactory.apply(config); + + // Verify filter creation + assertThat(filter) + .isInstanceOf(ContextGatewayFilter.class) + .isNotNull(); + } + + // Test shortcut field order configuration + @Test + void shouldReturnCorrectShortcutFieldOrder() { + // Execute & Verify + assertThat(filterFactory.shortcutFieldOrder()) + .containsExactly("group"); + } + + // Test config class getter/setter behavior + @Test + void shouldHandleConfigGroupPropertyCorrectly() { + // Setup config + ContextGatewayFilterFactory.Config config = new ContextGatewayFilterFactory.Config(); + final String expectedGroup = "payment-service"; + + // Test setter/getter + config.setGroup(expectedGroup); + assertThat(config.getGroup()) + .isEqualTo(expectedGroup); + } + + + // Test filter creation with empty group name + @Test + void shouldHandleEmptyGroupNameInConfig() { + // Setup config with empty group + ContextGatewayFilterFactory.Config config = new ContextGatewayFilterFactory.Config(); + config.setGroup(""); + + // Create filter instance + GatewayFilter filter = filterFactory.apply(config); + + // Verify filter creation + assertThat(filter) + .isNotNull() + .isInstanceOf(ContextGatewayFilter.class); + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterTest.java new file mode 100644 index 000000000..a34b440e1 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayFilterTest.java @@ -0,0 +1,157 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway.context; + +import java.net.URI; +import java.util.Collections; + +import com.tencent.cloud.common.constant.MetadataConstant; +import com.tencent.cloud.common.metadata.MetadataContextHolder; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import reactor.core.publisher.Mono; + +import org.springframework.cloud.gateway.filter.GatewayFilterChain; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; +import org.springframework.web.server.ServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_REQUEST_URL_ATTR; +import static org.springframework.cloud.gateway.support.ServerWebExchangeUtils.GATEWAY_ROUTE_ATTR; + +/** + * Test class for {@link ContextGatewayFilter}. + */ +class ContextGatewayFilterTest { + + @Mock + private ContextGatewayPropertiesManager mockManager; + @Mock + private GatewayFilterChain mockChain; + + private ContextGatewayFilter filter; + private GroupContext groupContext; + + @BeforeEach + void setUp() { + MockitoAnnotations.openMocks(this); + ContextGatewayFilterFactory.Config config = new ContextGatewayFilterFactory.Config(); + config.setGroup("testGroup"); + filter = new ContextGatewayFilter(mockManager, config); + groupContext = new GroupContext(); + when(mockManager.getGroups()).thenReturn(Collections.singletonMap("testGroup", groupContext)); + } + + // Test EXTERNAL API path reconstruction + @Test + void shouldHandleExternalApiPathCorrectly() { + // Setup group context + GroupContext.ContextPredicate predicate = createPredicate(ApiType.EXTERNAL, Position.PATH, Position.PATH); + groupContext.setPredicate(predicate); + groupContext.setRoutes(Collections.singletonList( + createContextRoute("GET|/external/api", "GET", "testNS", "testSvc") + )); + + // Create test request + MockServerHttpRequest request = MockServerHttpRequest.get("/context/external/api").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + GroupContext.ContextRoute route = createContextRoute("GET|/external/api", "GET", "http://test.com"); + + when(mockManager.getGroupPathRoute("testGroup", "GET|/external/api")).thenReturn(route); + + // Execute filter + Mono<Void> result = filter.filter(exchange, mockChain); + + // Verify final URL + URI finalUri = exchange.getRequiredAttribute(GATEWAY_REQUEST_URL_ATTR); + assertThat(finalUri.toString()).isEqualTo("http://test.com/external/api"); + } + + // Test metadata update functionality + @Test + void shouldUpdateRouteMetadataCorrectly() throws Exception { + // Setup context route with metadata + GroupContext.ContextRoute route = createContextRoute("GET|/api", "GET", "ns", "svc"); + route.setMetadata(Collections.singletonMap("version", "v2")); + + // Setup group context + GroupContext.ContextPredicate predicate = createPredicate(ApiType.MS, Position.PATH, Position.PATH); + groupContext.setPredicate(predicate); + groupContext.setRoutes(Collections.singletonList(route)); + + // Create test request + MockServerHttpRequest request = MockServerHttpRequest.get("/context/ns/svc/api").build(); + ServerWebExchange exchange = MockServerWebExchange.from(request); + exchange.getAttributes().put(GATEWAY_ROUTE_ATTR, Route.async().id("test").uri(URI.create("lb://svc")). + predicate((GatewayPredicate) serverWebExchange -> false).build()); + when(mockManager.getGroupPathRoute("testGroup", "GET|/ns/svc/api")).thenReturn(route); + + exchange.getAttributes().put(MetadataConstant.HeaderName.METADATA_CONTEXT, MetadataContextHolder.get()); + // Execute filter + filter.filter(exchange, mockChain); + +// Route updatedRoute = exchange.getRequiredAttribute(GATEWAY_ROUTE_ATTR); +// assertThat(updatedRoute.getMetadata()).containsEntry("version", "v2"); + // no context in exchange + exchange.getAttributes().remove(MetadataConstant.HeaderName.METADATA_CONTEXT); + filter.filter(exchange, mockChain); + + } + + // Helper method to create test predicate + private GroupContext.ContextPredicate createPredicate(ApiType apiType, Position nsPos, Position svcPos) { + GroupContext.ContextPredicate predicate = new GroupContext.ContextPredicate(); + predicate.setApiType(apiType); + + GroupContext.ContextNamespace namespace = new GroupContext.ContextNamespace(); + namespace.setPosition(nsPos); + namespace.setKey("ns-key"); + predicate.setNamespace(namespace); + + GroupContext.ContextService service = new GroupContext.ContextService(); + service.setPosition(svcPos); + service.setKey("svc-key"); + predicate.setService(service); + + return predicate; + } + + // Helper method to create context route + private GroupContext.ContextRoute createContextRoute(String path, String method, String ns, String svc) { + GroupContext.ContextRoute route = new GroupContext.ContextRoute(); + route.setPath(path); + route.setMethod(method); + route.setNamespace(ns); + route.setService(svc); + return route; + } + + private GroupContext.ContextRoute createContextRoute(String path, String method, String host) { + GroupContext.ContextRoute route = new GroupContext.ContextRoute(); + route.setPath(path); + route.setMethod(method); + route.setHost(host); + return route; + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManagerTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManagerTest.java new file mode 100644 index 000000000..2c558428c --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextGatewayPropertiesManagerTest.java @@ -0,0 +1,305 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway.context; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import com.tencent.cloud.polaris.discovery.PolarisDiscoveryClient; +import com.tencent.cloud.polaris.discovery.reactive.PolarisReactiveDiscoveryClient; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; +import reactor.core.publisher.Flux; + +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.when; + +/** + * Test for {@link ContextGatewayPropertiesManager}. + */ +class ContextGatewayPropertiesManagerTest { + + private ContextGatewayPropertiesManager manager; + private PolarisDiscoveryClient mockDiscoveryClient; + private PolarisReactiveDiscoveryClient mockReactiveClient; + + @BeforeEach + void setup() { + manager = new ContextGatewayPropertiesManager(); + mockDiscoveryClient = Mockito.mock(PolarisDiscoveryClient.class); + mockReactiveClient = Mockito.mock(PolarisReactiveDiscoveryClient.class); + } + + @Test + void shouldHandleEmptyGroupsWhenSettingRouteMap() { + // Test empty groups handling + manager.setGroupRouteMap(null); + assertThat(manager.getGroupPathRouteMap()).isEmpty(); + assertThat(manager.getGroups()).isNull(); + } + + @Test + void shouldClassifyRoutesByPathType() { + // Prepare test data + Map<String, GroupContext> groups = new HashMap<>(); + GroupContext group1 = new GroupContext(); + GroupContext.ContextRoute normalRoute = new GroupContext.ContextRoute(); + normalRoute.setPath("/api/v1/normal"); + normalRoute.setMethod("POST"); + normalRoute.setNamespace("testNS"); + normalRoute.setService("testSvc"); + GroupContext.ContextRoute wildcardRoute = new GroupContext.ContextRoute(); + wildcardRoute.setPath("/api/wildcard/**"); + wildcardRoute.setMethod("GET"); + wildcardRoute.setNamespace("testNS"); + wildcardRoute.setService("testSvc"); + group1.setRoutes(Arrays.asList(normalRoute, wildcardRoute)); + GroupContext.ContextPredicate predicate = new GroupContext.ContextPredicate(); + predicate.setApiType(ApiType.MS); + group1.setPredicate(predicate); + groups.put("group1", group1); + + // Execute + manager.setGroupRouteMap(groups); + + // Verify classification + Map<String, Map<String, GroupContext.ContextRoute>> pathMap = manager.getGroupPathRouteMap(); + Map<String, Map<String, GroupContext.ContextRoute>> wildcardMap = manager.getGroupWildcardPathRouteMap(); + assertThat(pathMap.get("group1")).hasSize(1); + assertThat(wildcardMap.get("group1")).hasSize(1); + + // Execute eager load + manager.eagerLoad(mockDiscoveryClient, null); + + when(mockReactiveClient.getInstances("testSvc")).thenReturn(Flux.fromIterable(Collections.emptyList())); + manager.eagerLoad(null, mockReactiveClient); + + manager.eagerLoad(null, null); + } + + @Test + void shouldMatchExactPathBeforeWildcard() { + // Setup test routes + GroupContext group = new GroupContext(); + GroupContext.ContextRoute exactRoute = new GroupContext.ContextRoute(); + exactRoute.setPath("/api/exact"); + exactRoute.setMethod("POST"); + exactRoute.setNamespace("testNS"); + exactRoute.setService("testSvc"); + GroupContext.ContextRoute wildcardRoute = new GroupContext.ContextRoute(); + wildcardRoute.setPath("/api/*/wildcard"); + wildcardRoute.setMethod("GET"); + wildcardRoute.setNamespace("testNS"); + wildcardRoute.setService("testSvc"); + + group.setRoutes(Arrays.asList(exactRoute, wildcardRoute)); + GroupContext.ContextPredicate predicate = new GroupContext.ContextPredicate(); + predicate.setApiType(ApiType.MS); + GroupContext.ContextNamespace namespace = new GroupContext.ContextNamespace(); + namespace.setPosition(Position.PATH); + predicate.setNamespace(namespace); + GroupContext.ContextService service = new GroupContext.ContextService(); + service.setPosition(Position.PATH); + predicate.setService(service); + group.setPredicate(predicate); + manager.setGroupRouteMap(Collections.singletonMap("testGroup", group)); + + + ContextGatewayFilter filter = new ContextGatewayFilter(manager, null); + MockServerHttpRequest request = MockServerHttpRequest.post("http://localhost/context/testNS/testSvc/api/exact").build(); + String[] apis = filter.rebuildMsApi(request, group, request.getPath().value()); + + // Test path matching + GroupContext.ContextRoute result = manager.getGroupPathRoute("testGroup", apis[0]); + assertThat(result).isEqualTo(exactRoute); + // Test wildcard matching + request = MockServerHttpRequest.get("http://localhost/context/testNS/testSvc/api/test/wildcard").build(); + apis = filter.rebuildMsApi(request, group, request.getPath().value()); + result = manager.getGroupPathRoute("testGroup", apis[0]); + assertThat(result).isEqualTo(wildcardRoute); + // Test non-matching + request = MockServerHttpRequest.get("http://localhost/context/testNS/testSvc/api/wildcard").build(); + apis = filter.rebuildMsApi(request, group, request.getPath().value()); + result = manager.getGroupPathRoute("testGroup", apis[0]); + assertThat(result).isNull(); + } + + // Helper method to create test context route + private GroupContext.ContextRoute createContextRoute(String path, String method, String namespace, String service) { + GroupContext.ContextRoute route = new GroupContext.ContextRoute(); + route.setPath(path); + route.setMethod(method); + route.setNamespace(namespace); + route.setService(service); + return route; + } + + // Helper method to create group context with configurable positions + private GroupContext createGroupContext(ApiType apiType, Position namespacePos, Position servicePos) { + GroupContext group = new GroupContext(); + GroupContext.ContextPredicate predicate = new GroupContext.ContextPredicate(); + predicate.setApiType(apiType); + + GroupContext.ContextNamespace namespace = new GroupContext.ContextNamespace(); + namespace.setPosition(namespacePos); + namespace.setKey("ns-key"); + predicate.setNamespace(namespace); + + GroupContext.ContextService service = new GroupContext.ContextService(); + service.setPosition(servicePos); + service.setKey("svc-key"); + predicate.setService(service); + + group.setPredicate(predicate); + return group; + } + + @Test + void shouldHandleMultiplePositionCombinations() { + // ns position PATH + testPositionCombination(ApiType.MS, Position.PATH, Position.PATH, + "/context/nsFromPath/svcFromPath/api/test", + "POST|/nsFromPath/svcFromPath/api/test", + "/api/test"); + + testPositionCombination(ApiType.MS, Position.PATH, Position.HEADER, + "/context/nsFromPath/api/test", + "POST|/nsFromPath/svcFromHeader/api/test", + "/api/test"); + + testPositionCombination(ApiType.MS, Position.PATH, Position.QUERY, + "/context/nsFromPath/api/test?svc-key=querySVC", + "POST|/nsFromPath/querySVC/api/test", + "/api/test"); + // ns position QUERY + testPositionCombination(ApiType.MS, Position.QUERY, Position.PATH, + "/context/svcFromPath/api/test?ns-key=queryNS", + "POST|/queryNS/svcFromPath/api/test", + "/api/test"); + + testPositionCombination(ApiType.MS, Position.QUERY, Position.QUERY, + "/context/api/test?ns-key=queryNS&svc-key=querySVC", + "POST|/queryNS/querySVC/api/test", + "/api/test"); + + testPositionCombination(ApiType.MS, Position.QUERY, Position.HEADER, + "/context/api/test?ns-key=queryNS", + "POST|/queryNS/svcFromHeader/api/test", + "/api/test"); + // ns position HEADER + testPositionCombination(ApiType.MS, Position.HEADER, Position.PATH, + "/context/svcFromPath/api/test", + "POST|/headerNS/svcFromPath/api/test", + "/api/test"); + + testPositionCombination(ApiType.MS, Position.HEADER, Position.QUERY, + "/context/api/test?svc-key=querySVC", + "POST|/headerNS/querySVC/api/test", + "/api/test"); + + testPositionCombination(ApiType.MS, Position.HEADER, Position.HEADER, + "/context/api/test", + "POST|/headerNS/svcFromHeader/api/test", + "/api/test"); + + + } + + private void testPositionCombination(ApiType apiType, Position namespacePos, Position servicePos, + String inputPath, String expectedMatchPath, String expectedRealPath) { + // Setup group with specified positions + GroupContext group = createGroupContext(apiType, namespacePos, servicePos); + group.setRoutes(Collections.singletonList( + createContextRoute(expectedMatchPath, "POST", "testNS", "testSvc") + )); + manager.setGroupRouteMap(Collections.singletonMap("testGroup", group)); + + // Build test request with appropriate parameters + MockServerHttpRequest.BaseBuilder<?> requestBuilder = MockServerHttpRequest.post(inputPath); + switch (namespacePos) { + case HEADER: + requestBuilder.header("ns-key", "headerNS"); + break; + case QUERY: + // Query param already in URL + break; + } + switch (servicePos) { + case HEADER: + requestBuilder.header("svc-key", "svcFromHeader"); + break; + case QUERY: + // Query param already in URL + break; + } + + ContextGatewayFilter filter = new ContextGatewayFilter(manager, null); + MockServerHttpRequest mockServerHttpRequest = requestBuilder.build(); + String[] apis = filter.rebuildMsApi(mockServerHttpRequest, group, mockServerHttpRequest.getPath().value()); + + // Verify path reconstruction + assertThat(apis[0]).isEqualTo(expectedMatchPath); + assertThat(apis[1]).isEqualTo(expectedRealPath); + } + + @Test + void shouldHandleExternalApiType() { + // Test EXTERNAL API type + GroupContext group = createGroupContext(ApiType.EXTERNAL, Position.PATH, Position.PATH); + group.setRoutes(Collections.singletonList( + createContextRoute("POST|/external/api", "POST", null, null) + )); + manager.setGroupRouteMap(Collections.singletonMap("externalGroup", group)); + + ContextGatewayFilter filter = new ContextGatewayFilter(manager, null); + String inputPath = "/context/external/api"; + String[] apis = filter.rebuildExternalApi( + MockServerHttpRequest.post(inputPath).build(), + inputPath + ); + + assertThat(apis[0]).isEqualTo("POST|/external/api"); + assertThat(apis[1]).isEqualTo("/external/api"); + } + + @Test + void testGroupContext() { + GroupContext group1 = new GroupContext(); + group1.setComment("testComment"); + Assertions.assertEquals("testComment", group1.getComment()); + + GroupContext.ContextPredicate contextPredicate = new GroupContext.ContextPredicate(); + + contextPredicate.setContext("testContext"); + Assertions.assertEquals("testContext", contextPredicate.getContext()); + + GroupContext.ContextRoute contextRoute = new GroupContext.ContextRoute(); + contextRoute.setPathMapping("testPathMapping"); + Assertions.assertEquals("testPathMapping", contextRoute.getPathMapping()); + contextRoute.setHost("testHost"); + Assertions.assertEquals("testHost", contextRoute.getHost()); + contextRoute.setMetadata(Collections.singletonMap("testKey", "testValue")); + Assertions.assertEquals(1, contextRoute.getMetadata().size()); + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextPropertiesRouteDefinitionLocatorTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextPropertiesRouteDefinitionLocatorTest.java new file mode 100644 index 000000000..9a24cea21 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextPropertiesRouteDefinitionLocatorTest.java @@ -0,0 +1,69 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway.context; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; + +import org.junit.jupiter.api.Test; +import reactor.core.publisher.Flux; +import reactor.test.StepVerifier; + +import org.springframework.cloud.gateway.route.RouteDefinition; + +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +/** + * Tests for {@link ContextPropertiesRouteDefinitionLocator}. + */ +class ContextPropertiesRouteDefinitionLocatorTest { + + @Test + void shouldReturnEmptyWhenNoRoutes() { + + ContextGatewayProperties mockProps = mock(ContextGatewayProperties.class); + when(mockProps.getRoutes()).thenReturn(Collections.emptyMap()); + + ContextPropertiesRouteDefinitionLocator locator = new ContextPropertiesRouteDefinitionLocator(mockProps); + + Flux<RouteDefinition> result = locator.getRouteDefinitions(); + StepVerifier.create(result) + .expectNextCount(0) + .verifyComplete(); + } + + @Test + void shouldReturnAllRouteDefinitions() { + + Map<String, RouteDefinition> testRoutes = new HashMap<>(); + testRoutes.put("route1", new RouteDefinition()); + testRoutes.put("route2", new RouteDefinition()); + + ContextGatewayProperties mockProps = mock(ContextGatewayProperties.class); + when(mockProps.getRoutes()).thenReturn(testRoutes); + + ContextPropertiesRouteDefinitionLocator locator = new ContextPropertiesRouteDefinitionLocator(mockProps); + + Flux<RouteDefinition> result = locator.getRouteDefinitions(); + StepVerifier.create(result) + .expectNextCount(2) + .verifyComplete(); + } +} diff --git a/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextRoutePredicateFactoryTest.java b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextRoutePredicateFactoryTest.java new file mode 100644 index 000000000..3cd9dbcc2 --- /dev/null +++ b/spring-cloud-tencent-plugin-starters/spring-cloud-starter-tencent-gateway-plugin/src/test/java/com/tencent/cloud/plugin/gateway/context/ContextRoutePredicateFactoryTest.java @@ -0,0 +1,99 @@ +/* + * Tencent is pleased to support the open source community by making spring-cloud-tencent available. + * + * Copyright (C) 2021 THL A29 Limited, a Tencent company. All rights reserved. + * + * Licensed under the BSD 3-Clause License (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://opensource.org/licenses/BSD-3-Clause + * + * 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 com.tencent.cloud.plugin.gateway.context; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cloud.gateway.handler.predicate.GatewayPredicate; +import org.springframework.cloud.gateway.route.Route; +import org.springframework.mock.http.server.reactive.MockServerHttpRequest; +import org.springframework.mock.web.server.MockServerWebExchange; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ContextRoutePredicateFactory}. + */ +class ContextRoutePredicateFactoryTest { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(ContextRoutePredicateFactory.class); + + @Test + void shouldCreateConfigWithGroup() { + contextRunner.run(context -> { + // Arrange + ContextRoutePredicateFactory factory = context.getBean(ContextRoutePredicateFactory.class); + factory.shortcutFieldOrder(); + + // Act + ContextRoutePredicateFactory.Config config = factory.newConfig(); + config.setGroup("g1"); + Assertions.assertEquals("g1", config.getGroup()); + + GatewayPredicate gatewayPredicate = (GatewayPredicate) factory.apply(config); + gatewayPredicate.toString(); + Assertions.assertTrue(gatewayPredicate.test(null)); + Assertions.assertEquals(config, gatewayPredicate.getConfig()); + }); + } + + @Test + void shouldAlwaysMatchWhenNotImplemented() { + contextRunner.run(context -> { + // Arrange + ContextRoutePredicateFactory factory = context.getBean(ContextRoutePredicateFactory.class); + MockServerWebExchange exchange = MockServerWebExchange.from( + MockServerHttpRequest.get("/test").build()); + + // Act + ContextRoutePredicateFactory.Config config = new ContextRoutePredicateFactory.Config(); + config.setGroup("test-group"); + + // Assert + boolean result = factory.apply(config).test(exchange); + assertThat(result).isTrue(); + }); + } + + @Test + void shouldSupportShortcutFieldOrder() { + contextRunner.run(context -> { + ContextRoutePredicateFactory factory = context.getBean(ContextRoutePredicateFactory.class); + assertThat(factory.shortcutFieldOrder()).containsExactly("group"); + }); + } + + @Test + void shouldCreateValidRoutePredicate() { + contextRunner.run(context -> { + // Arrange + Route route = Route.async() + .id("test-route") + .uri("http://example.com") + .predicate(context.getBean(ContextRoutePredicateFactory.class) + .apply(c -> c.setGroup("group1"))) + .build(); + + // Act + assertThat(route.getPredicate()).isNotNull(); + }); + } +} diff --git a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java index 0548842f4..01196015c 100644 --- a/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java +++ b/spring-cloud-tencent-rpc-enhancement/src/main/java/com/tencent/cloud/rpc/enhancement/instrument/scg/EnhancedGatewayGlobalFilter.java @@ -139,6 +139,12 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered { } }) .doOnSuccess(v -> { + MetadataContext metadataContextOnSuccess = originExchange.getAttribute( + MetadataConstant.HeaderName.METADATA_CONTEXT); + if (metadataContextOnSuccess != null) { + MetadataContextHolder.set(metadataContextOnSuccess); + } + enhancedPluginContext.setDelay(System.currentTimeMillis() - startTime); EnhancedResponseContext enhancedResponseContext = EnhancedResponseContext.builder() .httpStatus(exchange.getResponse().getRawStatusCode()) @@ -150,6 +156,12 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered { pluginRunner.run(EnhancedPluginType.Client.POST, enhancedPluginContext); }) .doOnError(t -> { + MetadataContext metadataContextOnError = originExchange.getAttribute( + MetadataConstant.HeaderName.METADATA_CONTEXT); + if (metadataContextOnError != null) { + MetadataContextHolder.set(metadataContextOnError); + } + enhancedPluginContext.setDelay(System.currentTimeMillis() - startTime); enhancedPluginContext.setThrowable(t); @@ -157,6 +169,12 @@ public class EnhancedGatewayGlobalFilter implements GlobalFilter, Ordered { pluginRunner.run(EnhancedPluginType.Client.EXCEPTION, enhancedPluginContext); }) .doFinally(v -> { + MetadataContext metadataContextOnFinally = originExchange.getAttribute( + MetadataConstant.HeaderName.METADATA_CONTEXT); + if (metadataContextOnFinally != null) { + MetadataContextHolder.set(metadataContextOnFinally); + } + // Run finally enhanced plugins. pluginRunner.run(EnhancedPluginType.Client.FINALLY, enhancedPluginContext); });