fix: prepend context-path to contract reporter API paths (#1803)

Signed-off-by: Haotian Zhang <928016560@qq.com>
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2021
Haotian Zhang 5 days ago committed by Haotian Zhang
parent 89f11674ad
commit 7bed37f6fa

@ -8,3 +8,4 @@
- [feat: support enable/disable cloud location provider via configuration.](https://github.com/Tencent/spring-cloud-tencent/pull/1799)
- [feat: refactor Feign eager load, add LoadBalancer warm-up, and fix gateway trailing slash compatibility](https://github.com/Tencent/spring-cloud-tencent/pull/1800)
- [test: add unit tests for ConfigurationModifier and fix TsfConsul report logic](https://github.com/Tencent/spring-cloud-tencent/pull/1802)
- [fix: prepend context-path to contract reporter API paths](https://github.com/Tencent/spring-cloud-tencent/pull/1803)

@ -70,16 +70,20 @@ public class PolarisContractReporter implements ApplicationListener<ApplicationR
private final ObjectMapperProvider springdocObjectMapperProvider;
private final String contextPath;
public PolarisContractReporter(org.springdoc.webmvc.api.MultipleOpenApiResource multipleOpenApiWebMvcResource,
org.springdoc.webflux.api.MultipleOpenApiResource multipleOpenApiWebFluxResource,
PolarisContractProperties polarisContractProperties, ProviderAPI providerAPI,
PolarisDiscoveryProperties polarisDiscoveryProperties, ObjectMapperProvider springdocObjectMapperProvider) {
PolarisDiscoveryProperties polarisDiscoveryProperties, ObjectMapperProvider springdocObjectMapperProvider,
String contextPath) {
this.multipleOpenApiWebMvcResource = multipleOpenApiWebMvcResource;
this.multipleOpenApiWebFluxResource = multipleOpenApiWebFluxResource;
this.polarisContractProperties = polarisContractProperties;
this.providerAPI = providerAPI;
this.polarisDiscoveryProperties = polarisDiscoveryProperties;
this.springdocObjectMapperProvider = springdocObjectMapperProvider;
this.contextPath = contextPath;
}
@Override
@ -110,6 +114,13 @@ public class PolarisContractReporter implements ApplicationListener<ApplicationR
request.setVersion(polarisDiscoveryProperties.getVersion());
List<InterfaceDescriptor> interfaceDescriptorList = getInterfaceDescriptorFromSwagger(openAPI);
request.setInterfaceDescriptors(interfaceDescriptorList);
if (StringUtils.isNotBlank(contextPath) && openAPI.getPaths() != null) {
Paths newPaths = new Paths();
for (Map.Entry<String, PathItem> entry : openAPI.getPaths().entrySet()) {
newPaths.addPathItem(contextPath + entry.getKey(), entry.getValue());
}
openAPI.setPaths(newPaths);
}
String jsonValue;
if (springdocObjectMapperProvider != null && springdocObjectMapperProvider.jsonMapper() != null) {
jsonValue = springdocObjectMapperProvider.jsonMapper().writeValueAsString(openAPI);
@ -143,6 +154,9 @@ public class PolarisContractReporter implements ApplicationListener<ApplicationR
private List<InterfaceDescriptor> getInterfaceDescriptorFromSwagger(OpenAPI openAPI) {
List<InterfaceDescriptor> interfaceDescriptorList = new ArrayList<>();
Paths paths = openAPI.getPaths();
if (paths == null) {
return interfaceDescriptorList;
}
for (Map.Entry<String, PathItem> p : paths.entrySet()) {
PathItem path = p.getValue();
Map<String, Operation> operationMap = getOperationMapFromPath(path);
@ -151,7 +165,12 @@ public class PolarisContractReporter implements ApplicationListener<ApplicationR
}
for (Map.Entry<String, Operation> o : operationMap.entrySet()) {
InterfaceDescriptor interfaceDescriptor = new InterfaceDescriptor();
interfaceDescriptor.setPath(p.getKey());
if (StringUtils.isNotBlank(contextPath)) {
interfaceDescriptor.setPath(contextPath + p.getKey());
}
else {
interfaceDescriptor.setPath(p.getKey());
}
interfaceDescriptor.setMethod(o.getKey());
try {
String jsonValue;

@ -41,6 +41,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplicat
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
@ -100,9 +101,18 @@ public class PolarisSwaggerAutoConfiguration {
@Nullable MultipleOpenApiWebMvcResource multipleOpenApiWebMvcResource,
@Nullable MultipleOpenApiWebFluxResource multipleOpenApiWebFluxResource,
PolarisContractProperties polarisContractProperties, PolarisSDKContextManager polarisSDKContextManager,
PolarisDiscoveryProperties polarisDiscoveryProperties, ObjectMapperProvider springdocObjectMapperProvider) {
PolarisDiscoveryProperties polarisDiscoveryProperties, ObjectMapperProvider springdocObjectMapperProvider,
Environment environment) {
String contextPath = environment.getProperty("server.servlet.context-path", "");
if (!StringUtils.hasText(contextPath)) {
contextPath = environment.getProperty("spring.webflux.base-path", "");
}
if (contextPath.endsWith("/")) {
contextPath = contextPath.substring(0, contextPath.length() - 1);
}
return new PolarisContractReporter(multipleOpenApiWebMvcResource, multipleOpenApiWebFluxResource,
polarisContractProperties, polarisSDKContextManager.getProviderAPI(), polarisDiscoveryProperties, springdocObjectMapperProvider);
polarisContractProperties, polarisSDKContextManager.getProviderAPI(), polarisDiscoveryProperties,
springdocObjectMapperProvider, contextPath);
}
@Bean

@ -17,12 +17,15 @@
package com.tencent.cloud.polaris.contract.tsf;
import java.util.Map;
import java.util.concurrent.atomic.AtomicBoolean;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.tencent.cloud.common.util.GzipUtil;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springdoc.api.AbstractOpenApiResource;
@ -45,15 +48,18 @@ public class TsfApiMetadataGrapher implements SmartLifecycle {
private final ObjectMapperProvider springdocObjectMapperProvider;
private final ApplicationContext applicationContext;
private final String groupName;
private final String contextPath;
public TsfApiMetadataGrapher(org.springdoc.webmvc.api.MultipleOpenApiResource multipleOpenApiWebMvcResource,
org.springdoc.webflux.api.MultipleOpenApiResource multipleOpenApiWebFluxResource,
String groupName, ApplicationContext applicationContext, ObjectMapperProvider springdocObjectMapperProvider) {
String groupName, ApplicationContext applicationContext, ObjectMapperProvider springdocObjectMapperProvider,
String contextPath) {
this.applicationContext = applicationContext;
this.multipleOpenApiWebMvcResource = multipleOpenApiWebMvcResource;
this.multipleOpenApiWebFluxResource = multipleOpenApiWebFluxResource;
this.groupName = groupName;
this.springdocObjectMapperProvider = springdocObjectMapperProvider;
this.contextPath = contextPath;
}
@Override
@ -84,6 +90,14 @@ public class TsfApiMetadataGrapher implements SmartLifecycle {
if (openApiResource != null) {
openAPI = AbstractOpenApiResourceUtil.getOpenApi(openApiResource);
}
if (openAPI != null && contextPath != null && !contextPath.isEmpty()
&& openAPI.getPaths() != null) {
Paths newPaths = new Paths();
for (Map.Entry<String, PathItem> entry : openAPI.getPaths().entrySet()) {
newPaths.addPathItem(contextPath + entry.getKey(), entry.getValue());
}
openAPI.setPaths(newPaths);
}
String jsonValue;
if (springdocObjectMapperProvider != null && springdocObjectMapperProvider.jsonMapper() != null) {
jsonValue = springdocObjectMapperProvider.jsonMapper().writeValueAsString(openAPI);

@ -28,7 +28,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
import org.springframework.lang.Nullable;
import org.springframework.util.StringUtils;
@Configuration
@ConditionalOnTsfConsulEnabled
@ -39,8 +41,16 @@ public class TsfSwaggerAutoConfiguration {
@ConditionalOnBean(OpenAPI.class)
public TsfApiMetadataGrapher tsfApiMetadataGrapher(@Nullable MultipleOpenApiWebMvcResource multipleOpenApiWebMvcResource,
@Nullable MultipleOpenApiWebFluxResource multipleOpenApiWebFluxResource, ApplicationContext context,
PolarisContractProperties polarisContractProperties, ObjectMapperProvider springdocObjectMapperProvider) {
PolarisContractProperties polarisContractProperties, ObjectMapperProvider springdocObjectMapperProvider,
Environment environment) {
String contextPath = environment.getProperty("server.servlet.context-path", "");
if (!StringUtils.hasText(contextPath)) {
contextPath = environment.getProperty("spring.webflux.base-path", "");
}
if (contextPath.endsWith("/")) {
contextPath = contextPath.substring(0, contextPath.length() - 1);
}
return new TsfApiMetadataGrapher(multipleOpenApiWebMvcResource, multipleOpenApiWebFluxResource,
polarisContractProperties.getGroup(), context, springdocObjectMapperProvider);
polarisContractProperties.getGroup(), context, springdocObjectMapperProvider, contextPath);
}
}

@ -0,0 +1,186 @@
/*
* Tencent is pleased to support the open source community by making spring-cloud-tencent available.
*
* Copyright (C) 2021 Tencent. 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.polaris.contract;
import java.lang.reflect.Method;
import java.util.List;
import com.tencent.cloud.polaris.PolarisDiscoveryProperties;
import com.tencent.cloud.polaris.contract.config.PolarisContractProperties;
import com.tencent.polaris.api.core.ProviderAPI;
import com.tencent.polaris.api.plugin.server.InterfaceDescriptor;
import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.Operation;
import io.swagger.v3.oas.models.PathItem;
import io.swagger.v3.oas.models.Paths;
import org.junit.jupiter.api.DisplayName;
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.springdoc.core.providers.ObjectMapperProvider;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Test for {@link PolarisContractReporter}.
*
* @author Haotian Zhang
*/
@DisplayName("PolarisContractReporter")
@ExtendWith(MockitoExtension.class)
class PolarisContractReporterTest {
@Mock
private PolarisContractProperties polarisContractProperties;
@Mock
private ProviderAPI providerAPI;
@Mock
private PolarisDiscoveryProperties polarisDiscoveryProperties;
@Mock
private ObjectMapperProvider springdocObjectMapperProvider;
private OpenAPI createTestOpenAPI() {
OpenAPI openAPI = new OpenAPI();
Paths paths = new Paths();
PathItem sumPath = new PathItem();
Operation sumGet = new Operation();
sumGet.setSummary("sum");
sumPath.setGet(sumGet);
paths.addPathItem("/quickstart/callee/sum", sumPath);
PathItem infoPath = new PathItem();
Operation infoGet = new Operation();
infoGet.setSummary("info");
infoPath.setGet(infoGet);
paths.addPathItem("/quickstart/callee/info", infoPath);
openAPI.setPaths(paths);
return openAPI;
}
@SuppressWarnings("unchecked")
private List<InterfaceDescriptor> invokeGetInterfaceDescriptorFromSwagger(
PolarisContractReporter reporter, OpenAPI openAPI) throws Exception {
Method method = PolarisContractReporter.class.getDeclaredMethod(
"getInterfaceDescriptorFromSwagger", OpenAPI.class);
method.setAccessible(true);
return (List<InterfaceDescriptor>) method.invoke(reporter, openAPI);
}
/**
* Test that context-path is prepended to InterfaceDescriptor paths.
*/
@DisplayName("context-path is prepended to interface descriptor paths")
@Test
void testContextPathPrependedToInterfaceDescriptorPaths() throws Exception {
// Arrange
String contextPath = "/callee-service";
PolarisContractReporter reporter = new PolarisContractReporter(
null, null, polarisContractProperties, providerAPI,
polarisDiscoveryProperties, springdocObjectMapperProvider, contextPath);
OpenAPI openAPI = createTestOpenAPI();
// Act
List<InterfaceDescriptor> descriptors = invokeGetInterfaceDescriptorFromSwagger(reporter, openAPI);
// Assert
assertThat(descriptors).hasSize(2);
assertThat(descriptors)
.extracting(InterfaceDescriptor::getPath)
.containsExactlyInAnyOrder(
"/callee-service/quickstart/callee/sum",
"/callee-service/quickstart/callee/info");
}
/**
* Test that empty context-path leaves paths unchanged.
*/
@DisplayName("empty context-path leaves paths unchanged")
@Test
void testEmptyContextPathLeavesPathsUnchanged() throws Exception {
// Arrange
String contextPath = "";
PolarisContractReporter reporter = new PolarisContractReporter(
null, null, polarisContractProperties, providerAPI,
polarisDiscoveryProperties, springdocObjectMapperProvider, contextPath);
OpenAPI openAPI = createTestOpenAPI();
// Act
List<InterfaceDescriptor> descriptors = invokeGetInterfaceDescriptorFromSwagger(reporter, openAPI);
// Assert
assertThat(descriptors).hasSize(2);
assertThat(descriptors)
.extracting(InterfaceDescriptor::getPath)
.containsExactlyInAnyOrder(
"/quickstart/callee/sum",
"/quickstart/callee/info");
}
/**
* Test that trailing slash on context-path is handled correctly via auto-config normalization.
* The auto-config strips trailing slashes before passing to the constructor,
* so we verify the reporter works correctly when contextPath has no trailing slash.
*/
@DisplayName("context-path without trailing slash works correctly")
@Test
void testContextPathWithoutTrailingSlash() throws Exception {
// Arrange
String contextPath = "/api";
PolarisContractReporter reporter = new PolarisContractReporter(
null, null, polarisContractProperties, providerAPI,
polarisDiscoveryProperties, springdocObjectMapperProvider, contextPath);
OpenAPI openAPI = createTestOpenAPI();
// Act
List<InterfaceDescriptor> descriptors = invokeGetInterfaceDescriptorFromSwagger(reporter, openAPI);
// Assert
assertThat(descriptors).hasSize(2);
assertThat(descriptors)
.extracting(InterfaceDescriptor::getPath)
.allMatch(path -> path.startsWith("/api/"));
}
/**
* Test that null paths in OpenAPI returns an empty descriptor list without NPE.
* When OpenAPI.getPaths() returns null, the method should gracefully return
* an empty list instead of throwing NullPointerException.
*/
@DisplayName("null paths in OpenAPI returns empty descriptor list")
@Test
void testNullPathsReturnsEmptyList() throws Exception {
// Arrange
PolarisContractReporter reporter = new PolarisContractReporter(
null, null, polarisContractProperties, providerAPI,
polarisDiscoveryProperties, springdocObjectMapperProvider, "/context");
OpenAPI openAPI = new OpenAPI();
// Do not call openAPI.setPaths(), so getPaths() returns null
// Act
List<InterfaceDescriptor> descriptors = invokeGetInterfaceDescriptorFromSwagger(reporter, openAPI);
// Assert
assertThat(descriptors).isNotNull().isEmpty();
}
}
Loading…
Cancel
Save