Add Actuator ApiVersion support and bump version
Add `ApiVersion` enum that can be injected into actuator endpoints if they need to support more than one API revision. Spring MVC, WebFlux and Jersey integrations now detect the API version based on the HTTP accept header. If the request explicitly accepts a `application/vnd.spring-boot.actuator.v` media type then the version is set from the header. If no explicit Spring Boot media type is accepted then the latest `ApiVersion` is assumed. A new v3 API revision has also been introduced to allow upcoming health endpoint format changes. By default all endpoints now consume and can produce v3, v2 and `application/json` media types. See gh-17929
This commit is contained in:
@@ -16,10 +16,8 @@
|
||||
|
||||
package org.springframework.boot.actuate.autoconfigure.endpoint.web;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -28,7 +26,6 @@ import org.springframework.boot.actuate.autoconfigure.endpoint.ExposeExcludeProp
|
||||
import org.springframework.boot.actuate.endpoint.EndpointFilter;
|
||||
import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
|
||||
import org.springframework.boot.actuate.endpoint.annotation.Endpoint;
|
||||
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvokerAdvisor;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
@@ -67,8 +64,6 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableConfigurationProperties(WebEndpointProperties.class)
|
||||
public class WebEndpointAutoConfiguration {
|
||||
|
||||
private static final List<String> MEDIA_TYPES = Arrays.asList(ActuatorMediaType.V2_JSON, "application/json");
|
||||
|
||||
private final ApplicationContext applicationContext;
|
||||
|
||||
private final WebEndpointProperties properties;
|
||||
@@ -86,7 +81,7 @@ public class WebEndpointAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public EndpointMediaTypes endpointMediaTypes() {
|
||||
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES);
|
||||
return EndpointMediaTypes.DEFAULT;
|
||||
}
|
||||
|
||||
@Bean
|
||||
|
||||
@@ -98,7 +98,7 @@ class CloudFoundryActuatorAutoConfigurationTests {
|
||||
"vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> {
|
||||
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
|
||||
mockMvc.perform(get("/cloudfoundryapplication"))
|
||||
.andExpect(header().string("Content-Type", ActuatorMediaType.V2_JSON));
|
||||
.andExpect(header().string("Content-Type", ActuatorMediaType.V3_JSON));
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -61,7 +61,8 @@ class WebEndpointAutoConfigurationTests {
|
||||
void webApplicationConfiguresEndpointMediaTypes() {
|
||||
this.contextRunner.run((context) -> {
|
||||
EndpointMediaTypes endpointMediaTypes = context.getBean(EndpointMediaTypes.class);
|
||||
assertThat(endpointMediaTypes.getConsumed()).containsExactly(ActuatorMediaType.V2_JSON, "application/json");
|
||||
assertThat(endpointMediaTypes.getConsumed()).containsExactly(ActuatorMediaType.V3_JSON,
|
||||
ActuatorMediaType.V2_JSON, "application/json");
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,7 @@ package org.springframework.boot.actuate.endpoint;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
@@ -25,6 +26,7 @@ import org.springframework.util.Assert;
|
||||
* The context for the {@link OperationInvoker invocation of an operation}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
* @author Phillip Webb
|
||||
* @since 2.0.0
|
||||
*/
|
||||
public class InvocationContext {
|
||||
@@ -33,23 +35,55 @@ public class InvocationContext {
|
||||
|
||||
private final Map<String, Object> arguments;
|
||||
|
||||
private final ApiVersion apiVersion;
|
||||
|
||||
/**
|
||||
* Creates a new context for an operation being invoked by the given {@code principal}
|
||||
* with the given available {@code arguments}.
|
||||
* Creates a new context for an operation being invoked by the given
|
||||
* {@code securityContext} with the given available {@code arguments}.
|
||||
* @param securityContext the current security context. Never {@code null}
|
||||
* @param arguments the arguments available to the operation. Never {@code null}
|
||||
*/
|
||||
public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments) {
|
||||
this(null, securityContext, arguments);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new context for an operation being invoked by the given
|
||||
* {@code securityContext} with the given available {@code arguments}.
|
||||
* @param apiVersion the API version or {@code null} to use the latest
|
||||
* @param securityContext the current security context. Never {@code null}
|
||||
* @param arguments the arguments available to the operation. Never {@code null}
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public InvocationContext(ApiVersion apiVersion, SecurityContext securityContext, Map<String, Object> arguments) {
|
||||
Assert.notNull(securityContext, "SecurityContext must not be null");
|
||||
Assert.notNull(arguments, "Arguments must not be null");
|
||||
this.apiVersion = (apiVersion != null) ? apiVersion : ApiVersion.LATEST;
|
||||
this.securityContext = securityContext;
|
||||
this.arguments = arguments;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the API version in use.
|
||||
* @return the apiVersion the API version
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public ApiVersion getApiVersion() {
|
||||
return this.apiVersion;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the security context to use for the invocation.
|
||||
* @return the security context
|
||||
*/
|
||||
public SecurityContext getSecurityContext() {
|
||||
return this.securityContext;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the invocation arguments.
|
||||
* @return the arguments
|
||||
*/
|
||||
public Map<String, Object> getArguments() {
|
||||
return this.arguments;
|
||||
}
|
||||
|
||||
@@ -27,14 +27,21 @@ public final class ActuatorMediaType {
|
||||
|
||||
/**
|
||||
* Constant for the Actuator V1 media type.
|
||||
* @deprecated since 2.2.0 as the v1 format is no longer supported
|
||||
*/
|
||||
@Deprecated
|
||||
public static final String V1_JSON = "application/vnd.spring-boot.actuator.v1+json";
|
||||
|
||||
/**
|
||||
* Constant for the Actuator V2 media type.
|
||||
* Constant for the Actuator {@link ApiVersion#V2 v2} media type.
|
||||
*/
|
||||
public static final String V2_JSON = "application/vnd.spring-boot.actuator.v2+json";
|
||||
|
||||
/**
|
||||
* Constant for the Actuator {@link ApiVersion#V3 v3} media type.
|
||||
*/
|
||||
public static final String V3_JSON = "application/vnd.spring-boot.actuator.v3+json";
|
||||
|
||||
private ActuatorMediaType() {
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.springframework.boot.actuate.endpoint.http;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.MimeTypeUtils;
|
||||
|
||||
/**
|
||||
* API versions supported for the actuator HTTP API. This enum may be injected into
|
||||
* actuator endpoints in order to return a response compatible with the requested version.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public enum ApiVersion {
|
||||
|
||||
/**
|
||||
* Version 2 (supported by Spring Boot 2.0+).
|
||||
*/
|
||||
V2,
|
||||
|
||||
/**
|
||||
* Version 3 (supported by Spring Boot 2.2+).
|
||||
*/
|
||||
V3;
|
||||
|
||||
private static final String MEDIA_TYPE_PREFIX = "application/vnd.spring-boot.actuator.";
|
||||
|
||||
/**
|
||||
* The latest API version.
|
||||
*/
|
||||
public static final ApiVersion LATEST = ApiVersion.V3;
|
||||
|
||||
/**
|
||||
* Return the {@link ApiVersion} to use based on the HTTP request headers. The version
|
||||
* will be deduced based on the {@code Accept} header.
|
||||
* @param headers the HTTP headers
|
||||
* @return the API version to use
|
||||
*/
|
||||
public static ApiVersion fromHttpHeaders(Map<String, List<String>> headers) {
|
||||
ApiVersion version = null;
|
||||
List<String> accepts = headers.get("Accept");
|
||||
if (!CollectionUtils.isEmpty(accepts)) {
|
||||
for (String accept : accepts) {
|
||||
for (String type : MimeTypeUtils.tokenize(accept)) {
|
||||
version = mostRecent(version, forType(type));
|
||||
}
|
||||
}
|
||||
}
|
||||
return (version != null) ? version : LATEST;
|
||||
}
|
||||
|
||||
private static ApiVersion forType(String type) {
|
||||
if (type.startsWith(MEDIA_TYPE_PREFIX)) {
|
||||
type = type.substring(MEDIA_TYPE_PREFIX.length());
|
||||
int suffixIndex = type.indexOf("+");
|
||||
type = (suffixIndex != -1) ? type.substring(0, suffixIndex) : type;
|
||||
try {
|
||||
return valueOf(type.toUpperCase());
|
||||
}
|
||||
catch (IllegalArgumentException ex) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private static ApiVersion mostRecent(ApiVersion existing, ApiVersion candidate) {
|
||||
int existingOrdinal = (existing != null) ? existing.ordinal() : -1;
|
||||
int candidateOrdinal = (candidate != null) ? candidate.ordinal() : -1;
|
||||
return (candidateOrdinal > existingOrdinal) ? candidate : existing;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -23,6 +23,7 @@ import java.util.stream.Collectors;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
|
||||
@@ -88,6 +89,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
|
||||
if (!parameter.isMandatory()) {
|
||||
return false;
|
||||
}
|
||||
if (ApiVersion.class.equals(parameter.getType())) {
|
||||
return false;
|
||||
}
|
||||
if (Principal.class.equals(parameter.getType())) {
|
||||
return context.getSecurityContext().getPrincipal() == null;
|
||||
}
|
||||
@@ -103,6 +107,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
|
||||
}
|
||||
|
||||
private Object resolveArgument(OperationParameter parameter, InvocationContext context) {
|
||||
if (ApiVersion.class.equals(parameter.getType())) {
|
||||
return context.getApiVersion();
|
||||
}
|
||||
if (Principal.class.equals(parameter.getType())) {
|
||||
return context.getSecurityContext().getPrincipal();
|
||||
}
|
||||
|
||||
@@ -16,9 +16,11 @@
|
||||
|
||||
package org.springframework.boot.actuate.endpoint.web;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
/**
|
||||
@@ -29,10 +31,40 @@ import org.springframework.util.Assert;
|
||||
*/
|
||||
public class EndpointMediaTypes {
|
||||
|
||||
private static final String JSON_MEDIA_TYPE = "application/json";
|
||||
|
||||
/**
|
||||
* Default {@link EndpointMediaTypes} for this version of Spring Boot.
|
||||
*/
|
||||
public static final EndpointMediaTypes DEFAULT = new EndpointMediaTypes(ActuatorMediaType.V3_JSON,
|
||||
ActuatorMediaType.V2_JSON, JSON_MEDIA_TYPE);
|
||||
|
||||
private final List<String> produced;
|
||||
|
||||
private final List<String> consumed;
|
||||
|
||||
/**
|
||||
* Creates a new {@link EndpointMediaTypes} with the given {@code produced} and
|
||||
* {@code consumed} media types.
|
||||
* @param producedAndConsumed the default media types that are produced and consumed
|
||||
* by an endpoint. Must not be {@code null}.
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public EndpointMediaTypes(String... producedAndConsumed) {
|
||||
this((producedAndConsumed != null) ? Arrays.asList(producedAndConsumed) : (List<String>) null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link EndpointMediaTypes} with the given {@code produced} and
|
||||
* {@code consumed} media types.
|
||||
* @param producedAndConsumed the default media types that are produced and consumed
|
||||
* by an endpoint. Must not be {@code null}.
|
||||
* @since 2.2.0
|
||||
*/
|
||||
public EndpointMediaTypes(List<String> producedAndConsumed) {
|
||||
this(producedAndConsumed, producedAndConsumed);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a new {@link EndpointMediaTypes} with the given {@code produced} and
|
||||
* {@code consumed} media types.
|
||||
|
||||
@@ -43,6 +43,7 @@ import reactor.core.publisher.Mono;
|
||||
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
@@ -150,8 +151,10 @@ public class JerseyEndpointResourceFactory {
|
||||
arguments.putAll(extractPathParameters(data));
|
||||
arguments.putAll(extractQueryParameters(data));
|
||||
try {
|
||||
Object response = this.operation
|
||||
.invoke(new InvocationContext(new JerseySecurityContext(data.getSecurityContext()), arguments));
|
||||
ApiVersion apiVersion = ApiVersion.fromHttpHeaders(data.getHeaders());
|
||||
JerseySecurityContext securityContext = new JerseySecurityContext(data.getSecurityContext());
|
||||
InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments);
|
||||
Object response = this.operation.invoke(invocationContext);
|
||||
return convertToJaxRsResponse(response, data.getRequest().getMethod());
|
||||
}
|
||||
catch (InvalidEndpointRequestException ex) {
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
@@ -309,6 +310,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) {
|
||||
ApiVersion apiVersion = ApiVersion.fromHttpHeaders(exchange.getRequest().getHeaders());
|
||||
Map<String, Object> arguments = getArguments(exchange, body);
|
||||
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
|
||||
.getMatchAllRemainingPathSegmentsVariable();
|
||||
@@ -317,7 +319,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
|
||||
tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable)));
|
||||
}
|
||||
return this.securityContextSupplier.get()
|
||||
.map((securityContext) -> new InvocationContext(securityContext, arguments))
|
||||
.map((securityContext) -> new InvocationContext(apiVersion, securityContext, arguments))
|
||||
.flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
|
||||
exchange.getRequest().getMethod()));
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.boot.actuate.endpoint.InvalidEndpointRequestException;
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
|
||||
@@ -40,9 +41,11 @@ import org.springframework.boot.actuate.endpoint.web.ExposableWebEndpoint;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperation;
|
||||
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.http.server.ServletServerHttpRequest;
|
||||
import org.springframework.util.AntPathMatcher;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
@@ -293,11 +296,13 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
|
||||
|
||||
@Override
|
||||
public Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) {
|
||||
HttpHeaders headers = new ServletServerHttpRequest(request).getHeaders();
|
||||
Map<String, Object> arguments = getArguments(request, body);
|
||||
try {
|
||||
return handleResult(
|
||||
this.operation.invoke(new InvocationContext(new ServletSecurityContext(request), arguments)),
|
||||
HttpMethod.valueOf(request.getMethod()));
|
||||
ApiVersion apiVersion = ApiVersion.fromHttpHeaders(headers);
|
||||
ServletSecurityContext securityContext = new ServletSecurityContext(request);
|
||||
InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments);
|
||||
return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod()));
|
||||
}
|
||||
catch (InvalidEndpointRequestException ex) {
|
||||
throw new BadOperationRequestException(ex.getReason());
|
||||
|
||||
@@ -0,0 +1,77 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.springframework.boot.actuate.endpoint;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link InvocationContext}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class InvocationContextTests {
|
||||
|
||||
private final SecurityContext securityContext = mock(SecurityContext.class);
|
||||
|
||||
private final Map<String, Object> arguments = Collections.singletonMap("test", "value");
|
||||
|
||||
@Test
|
||||
void createWhenApiVersionIsNullUsesLatestVersion() {
|
||||
InvocationContext context = new InvocationContext(null, this.securityContext, this.arguments);
|
||||
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.LATEST);
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenSecurityContextIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(null, this.arguments))
|
||||
.withMessage("SecurityContext must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenArgumentsIsNullThrowsException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new InvocationContext(this.securityContext, null))
|
||||
.withMessage("Arguments must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getApiVersionReturnsApiVersion() {
|
||||
InvocationContext context = new InvocationContext(ApiVersion.V2, this.securityContext, this.arguments);
|
||||
assertThat(context.getApiVersion()).isEqualTo(ApiVersion.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getSecurityContextReturnsSecurityContext() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
assertThat(context.getSecurityContext()).isEqualTo(this.securityContext);
|
||||
}
|
||||
|
||||
@Test
|
||||
void getArgumentsReturnsArguments() {
|
||||
InvocationContext context = new InvocationContext(this.securityContext, this.arguments);
|
||||
assertThat(context.getArguments()).isEqualTo(this.arguments);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2012-2019 the original author or authors.
|
||||
*
|
||||
* Licensed 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
|
||||
*
|
||||
* https://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.springframework.boot.actuate.endpoint.http;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link ApiVersion}.
|
||||
*
|
||||
* @author Phillip Webb
|
||||
*/
|
||||
class ApiVersionTests {
|
||||
|
||||
@Test
|
||||
void latestIsLatestVersion() {
|
||||
ApiVersion[] values = ApiVersion.values();
|
||||
assertThat(ApiVersion.LATEST).isEqualTo(values[values.length - 1]);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenEmptyReturnsLatest() {
|
||||
ApiVersion version = ApiVersion.fromHttpHeaders(Collections.emptyMap());
|
||||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenHasSingleV2HeaderReturnsV2() {
|
||||
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON));
|
||||
assertThat(version).isEqualTo(ApiVersion.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenHasSingleV3HeaderReturnsV3() {
|
||||
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader(ActuatorMediaType.V3_JSON));
|
||||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenHasV2AndV3HeaderReturnsV3() {
|
||||
ApiVersion version = ApiVersion
|
||||
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON, ActuatorMediaType.V3_JSON));
|
||||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenHasV2AndV3AsOneHeaderReturnsV3() {
|
||||
ApiVersion version = ApiVersion
|
||||
.fromHttpHeaders(acceptHeader(ActuatorMediaType.V2_JSON + "," + ActuatorMediaType.V3_JSON));
|
||||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenHasSingleHeaderWithoutJsonReturnsHeader() {
|
||||
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v2"));
|
||||
assertThat(version).isEqualTo(ApiVersion.V2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void fromHttpHeadersWhenHasUnknownVersionReturnsLatest() {
|
||||
ApiVersion version = ApiVersion.fromHttpHeaders(acceptHeader("application/vnd.spring-boot.actuator.v200"));
|
||||
assertThat(version).isEqualTo(ApiVersion.V3);
|
||||
}
|
||||
|
||||
private Map<String, List<String>> acceptHeader(String... types) {
|
||||
List<String> value = Arrays.asList(types);
|
||||
return value.isEmpty() ? Collections.emptyMap() : Collections.singletonMap("Accept", value);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.actuate.endpoint.InvocationContext;
|
||||
import org.springframework.boot.actuate.endpoint.OperationType;
|
||||
import org.springframework.boot.actuate.endpoint.SecurityContext;
|
||||
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.MissingParametersException;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
|
||||
import org.springframework.lang.Nullable;
|
||||
@@ -50,8 +51,8 @@ class ReflectiveOperationInvokerTests {
|
||||
@BeforeEach
|
||||
void setup() {
|
||||
this.target = new Example();
|
||||
this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse", String.class),
|
||||
OperationType.READ);
|
||||
this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse",
|
||||
ApiVersion.class, SecurityContext.class, String.class), OperationType.READ);
|
||||
this.parameterValueMapper = (parameter, value) -> (value != null) ? value.toString() : null;
|
||||
}
|
||||
|
||||
@@ -95,8 +96,8 @@ class ReflectiveOperationInvokerTests {
|
||||
|
||||
@Test
|
||||
void invokeWhenMissingNullableArgumentShouldInvoke() {
|
||||
OperationMethod operationMethod = new OperationMethod(
|
||||
ReflectionUtils.findMethod(Example.class, "reverseNullable", String.class), OperationType.READ);
|
||||
OperationMethod operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class,
|
||||
"reverseNullable", ApiVersion.class, SecurityContext.class, String.class), OperationType.READ);
|
||||
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, operationMethod,
|
||||
this.parameterValueMapper);
|
||||
Object result = invoker
|
||||
@@ -115,11 +116,15 @@ class ReflectiveOperationInvokerTests {
|
||||
|
||||
static class Example {
|
||||
|
||||
String reverse(String name) {
|
||||
String reverse(ApiVersion apiVersion, SecurityContext securityContext, String name) {
|
||||
assertThat(apiVersion).isEqualTo(ApiVersion.LATEST);
|
||||
assertThat(securityContext).isNotNull();
|
||||
return new StringBuilder(name).reverse().toString();
|
||||
}
|
||||
|
||||
String reverseNullable(@Nullable String name) {
|
||||
String reverseNullable(ApiVersion apiVersion, SecurityContext securityContext, @Nullable String name) {
|
||||
assertThat(apiVersion).isEqualTo(ApiVersion.LATEST);
|
||||
assertThat(securityContext).isNotNull();
|
||||
return new StringBuilder(String.valueOf(name)).reverse().toString();
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,8 @@ import java.util.List;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
|
||||
@@ -32,6 +34,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
|
||||
*/
|
||||
class EndpointMediaTypesTests {
|
||||
|
||||
@Test
|
||||
void defaultReturnsExpectedProducedAndConsumedTypes() {
|
||||
assertThat(EndpointMediaTypes.DEFAULT.getProduced()).containsExactly(ActuatorMediaType.V3_JSON,
|
||||
ActuatorMediaType.V2_JSON, "application/json");
|
||||
assertThat(EndpointMediaTypes.DEFAULT.getConsumed()).containsExactly(ActuatorMediaType.V3_JSON,
|
||||
ActuatorMediaType.V2_JSON, "application/json");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createWhenProducedIsNullShouldThrowException() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> new EndpointMediaTypes(null, Collections.emptyList()))
|
||||
@@ -44,6 +54,13 @@ class EndpointMediaTypesTests {
|
||||
.withMessageContaining("Consumed must not be null");
|
||||
}
|
||||
|
||||
@Test
|
||||
void createFromProducedAndConsumedUsesSameListForBoth() {
|
||||
EndpointMediaTypes types = new EndpointMediaTypes("spring/framework", "spring/boot");
|
||||
assertThat(types.getProduced()).containsExactly("spring/framework", "spring/boot");
|
||||
assertThat(types.getConsumed()).containsExactly("spring/framework", "spring/boot");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getProducedShouldReturnProduced() {
|
||||
List<String> produced = Arrays.asList("a", "b", "c");
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
package org.springframework.boot.actuate.endpoint.web.test;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
@@ -38,7 +37,6 @@ import org.junit.jupiter.api.extension.ParameterResolver;
|
||||
import org.junit.jupiter.api.extension.TestTemplateInvocationContext;
|
||||
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider;
|
||||
|
||||
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
|
||||
import org.springframework.boot.actuate.endpoint.invoke.convert.ConversionServiceParameterValueMapper;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
|
||||
import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
|
||||
@@ -68,7 +66,6 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.MergedAnnotations;
|
||||
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.server.reactive.HttpHandler;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.ClassUtils;
|
||||
@@ -233,9 +230,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
||||
}
|
||||
|
||||
private void customize(ResourceConfig config) {
|
||||
List<String> mediaTypes = Arrays.asList(javax.ws.rs.core.MediaType.APPLICATION_JSON,
|
||||
ActuatorMediaType.V2_JSON);
|
||||
EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes);
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
@@ -281,8 +276,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
||||
|
||||
@Bean
|
||||
WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() {
|
||||
List<String> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON);
|
||||
EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes);
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
@@ -311,8 +305,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
|
||||
|
||||
@Bean
|
||||
WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() {
|
||||
List<String> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON);
|
||||
EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes);
|
||||
EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
|
||||
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
|
||||
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
|
||||
Collections.emptyList());
|
||||
|
||||
Reference in New Issue
Block a user