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
pull/18371/head
Phillip Webb 5 years ago
parent d83238aaab
commit deb9d67cef

@ -16,10 +16,8 @@
package org.springframework.boot.actuate.autoconfigure.endpoint.web; package org.springframework.boot.actuate.autoconfigure.endpoint.web;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import org.springframework.beans.factory.ObjectProvider; 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.EndpointFilter;
import org.springframework.boot.actuate.endpoint.EndpointsSupplier; import org.springframework.boot.actuate.endpoint.EndpointsSupplier;
import org.springframework.boot.actuate.endpoint.annotation.Endpoint; 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.OperationInvokerAdvisor;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -67,8 +64,6 @@ import org.springframework.context.annotation.Configuration;
@EnableConfigurationProperties(WebEndpointProperties.class) @EnableConfigurationProperties(WebEndpointProperties.class)
public class WebEndpointAutoConfiguration { public class WebEndpointAutoConfiguration {
private static final List<String> MEDIA_TYPES = Arrays.asList(ActuatorMediaType.V2_JSON, "application/json");
private final ApplicationContext applicationContext; private final ApplicationContext applicationContext;
private final WebEndpointProperties properties; private final WebEndpointProperties properties;
@ -86,7 +81,7 @@ public class WebEndpointAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public EndpointMediaTypes endpointMediaTypes() { public EndpointMediaTypes endpointMediaTypes() {
return new EndpointMediaTypes(MEDIA_TYPES, MEDIA_TYPES); return EndpointMediaTypes.DEFAULT;
} }
@Bean @Bean

@ -98,7 +98,7 @@ class CloudFoundryActuatorAutoConfigurationTests {
"vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> { "vcap.application.cf_api:https://my-cloud-controller.com").run((context) -> {
MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build(); MockMvc mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
mockMvc.perform(get("/cloudfoundryapplication")) 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() { void webApplicationConfiguresEndpointMediaTypes() {
this.contextRunner.run((context) -> { this.contextRunner.run((context) -> {
EndpointMediaTypes endpointMediaTypes = context.getBean(EndpointMediaTypes.class); 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 java.util.Map;
import org.springframework.boot.actuate.endpoint.http.ApiVersion;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.util.Assert; import org.springframework.util.Assert;
@ -25,6 +26,7 @@ import org.springframework.util.Assert;
* The context for the {@link OperationInvoker invocation of an operation}. * The context for the {@link OperationInvoker invocation of an operation}.
* *
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb
* @since 2.0.0 * @since 2.0.0
*/ */
public class InvocationContext { public class InvocationContext {
@ -33,23 +35,55 @@ public class InvocationContext {
private final Map<String, Object> arguments; private final Map<String, Object> arguments;
private final ApiVersion apiVersion;
/** /**
* Creates a new context for an operation being invoked by the given {@code principal} * Creates a new context for an operation being invoked by the given
* with the given available {@code arguments}. * {@code securityContext} with the given available {@code arguments}.
* @param securityContext the current security context. Never {@code null} * @param securityContext the current security context. Never {@code null}
* @param arguments the arguments available to the operation. Never {@code null} * @param arguments the arguments available to the operation. Never {@code null}
*/ */
public InvocationContext(SecurityContext securityContext, Map<String, Object> arguments) { 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(securityContext, "SecurityContext must not be null");
Assert.notNull(arguments, "Arguments must not be null"); Assert.notNull(arguments, "Arguments must not be null");
this.apiVersion = (apiVersion != null) ? apiVersion : ApiVersion.LATEST;
this.securityContext = securityContext; this.securityContext = securityContext;
this.arguments = arguments; 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() { public SecurityContext getSecurityContext() {
return this.securityContext; return this.securityContext;
} }
/**
* Return the invocation arguments.
* @return the arguments
*/
public Map<String, Object> getArguments() { public Map<String, Object> getArguments() {
return this.arguments; return this.arguments;
} }

@ -27,14 +27,21 @@ public final class ActuatorMediaType {
/** /**
* Constant for the Actuator V1 media type. * 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"; 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"; 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() { 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.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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.MissingParametersException;
import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker; import org.springframework.boot.actuate.endpoint.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.invoke.OperationParameter; import org.springframework.boot.actuate.endpoint.invoke.OperationParameter;
@ -88,6 +89,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
if (!parameter.isMandatory()) { if (!parameter.isMandatory()) {
return false; return false;
} }
if (ApiVersion.class.equals(parameter.getType())) {
return false;
}
if (Principal.class.equals(parameter.getType())) { if (Principal.class.equals(parameter.getType())) {
return context.getSecurityContext().getPrincipal() == null; return context.getSecurityContext().getPrincipal() == null;
} }
@ -103,6 +107,9 @@ public class ReflectiveOperationInvoker implements OperationInvoker {
} }
private Object resolveArgument(OperationParameter parameter, InvocationContext context) { private Object resolveArgument(OperationParameter parameter, InvocationContext context) {
if (ApiVersion.class.equals(parameter.getType())) {
return context.getApiVersion();
}
if (Principal.class.equals(parameter.getType())) { if (Principal.class.equals(parameter.getType())) {
return context.getSecurityContext().getPrincipal(); return context.getSecurityContext().getPrincipal();
} }

@ -16,9 +16,11 @@
package org.springframework.boot.actuate.endpoint.web; package org.springframework.boot.actuate.endpoint.web;
import java.util.Arrays;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.springframework.boot.actuate.endpoint.http.ActuatorMediaType;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@ -29,10 +31,40 @@ import org.springframework.util.Assert;
*/ */
public class EndpointMediaTypes { 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> produced;
private final List<String> consumed; 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 * Creates a new {@link EndpointMediaTypes} with the given {@code produced} and
* {@code consumed} media types. * {@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.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -150,8 +151,10 @@ public class JerseyEndpointResourceFactory {
arguments.putAll(extractPathParameters(data)); arguments.putAll(extractPathParameters(data));
arguments.putAll(extractQueryParameters(data)); arguments.putAll(extractQueryParameters(data));
try { try {
Object response = this.operation ApiVersion apiVersion = ApiVersion.fromHttpHeaders(data.getHeaders());
.invoke(new InvocationContext(new JerseySecurityContext(data.getSecurityContext()), arguments)); 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()); return convertToJaxRsResponse(response, data.getRequest().getMethod());
} }
catch (InvalidEndpointRequestException ex) { 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.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes;
@ -309,6 +310,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
@Override @Override
public Mono<ResponseEntity<Object>> handle(ServerWebExchange exchange, Map<String, String> body) { 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); Map<String, Object> arguments = getArguments(exchange, body);
String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate() String matchAllRemainingPathSegmentsVariable = this.operation.getRequestPredicate()
.getMatchAllRemainingPathSegmentsVariable(); .getMatchAllRemainingPathSegmentsVariable();
@ -317,7 +319,7 @@ public abstract class AbstractWebFluxEndpointHandlerMapping extends RequestMappi
tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable))); tokenizePathSegments((String) arguments.get(matchAllRemainingPathSegmentsVariable)));
} }
return this.securityContextSupplier.get() 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), .flatMap((invocationContext) -> handleResult((Publisher<?>) this.invoker.invoke(invocationContext),
exchange.getRequest().getMethod())); 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.InvalidEndpointRequestException;
import org.springframework.boot.actuate.endpoint.InvocationContext; import org.springframework.boot.actuate.endpoint.InvocationContext;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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.invoke.OperationInvoker;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; import org.springframework.boot.actuate.endpoint.web.EndpointMapping;
import org.springframework.boot.actuate.endpoint.web.EndpointMediaTypes; 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.WebEndpointResponse;
import org.springframework.boot.actuate.endpoint.web.WebOperation; import org.springframework.boot.actuate.endpoint.web.WebOperation;
import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate; import org.springframework.boot.actuate.endpoint.web.WebOperationRequestPredicate;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.http.server.ServletServerHttpRequest;
import org.springframework.util.AntPathMatcher; import org.springframework.util.AntPathMatcher;
import org.springframework.util.Assert; import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@ -293,11 +296,13 @@ public abstract class AbstractWebMvcEndpointHandlerMapping extends RequestMappin
@Override @Override
public Object handle(HttpServletRequest request, @RequestBody(required = false) Map<String, String> body) { 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); Map<String, Object> arguments = getArguments(request, body);
try { try {
return handleResult( ApiVersion apiVersion = ApiVersion.fromHttpHeaders(headers);
this.operation.invoke(new InvocationContext(new ServletSecurityContext(request), arguments)), ServletSecurityContext securityContext = new ServletSecurityContext(request);
HttpMethod.valueOf(request.getMethod())); InvocationContext invocationContext = new InvocationContext(apiVersion, securityContext, arguments);
return handleResult(this.operation.invoke(invocationContext), HttpMethod.resolve(request.getMethod()));
} }
catch (InvalidEndpointRequestException ex) { catch (InvalidEndpointRequestException ex) {
throw new BadOperationRequestException(ex.getReason()); 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.InvocationContext;
import org.springframework.boot.actuate.endpoint.OperationType; import org.springframework.boot.actuate.endpoint.OperationType;
import org.springframework.boot.actuate.endpoint.SecurityContext; 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.MissingParametersException;
import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper; import org.springframework.boot.actuate.endpoint.invoke.ParameterValueMapper;
import org.springframework.lang.Nullable; import org.springframework.lang.Nullable;
@ -50,8 +51,8 @@ class ReflectiveOperationInvokerTests {
@BeforeEach @BeforeEach
void setup() { void setup() {
this.target = new Example(); this.target = new Example();
this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse", String.class), this.operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class, "reverse",
OperationType.READ); ApiVersion.class, SecurityContext.class, String.class), OperationType.READ);
this.parameterValueMapper = (parameter, value) -> (value != null) ? value.toString() : null; this.parameterValueMapper = (parameter, value) -> (value != null) ? value.toString() : null;
} }
@ -95,8 +96,8 @@ class ReflectiveOperationInvokerTests {
@Test @Test
void invokeWhenMissingNullableArgumentShouldInvoke() { void invokeWhenMissingNullableArgumentShouldInvoke() {
OperationMethod operationMethod = new OperationMethod( OperationMethod operationMethod = new OperationMethod(ReflectionUtils.findMethod(Example.class,
ReflectionUtils.findMethod(Example.class, "reverseNullable", String.class), OperationType.READ); "reverseNullable", ApiVersion.class, SecurityContext.class, String.class), OperationType.READ);
ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, operationMethod, ReflectiveOperationInvoker invoker = new ReflectiveOperationInvoker(this.target, operationMethod,
this.parameterValueMapper); this.parameterValueMapper);
Object result = invoker Object result = invoker
@ -115,11 +116,15 @@ class ReflectiveOperationInvokerTests {
static class Example { 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(); 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(); return new StringBuilder(String.valueOf(name)).reverse().toString();
} }

@ -22,6 +22,8 @@ import java.util.List;
import org.junit.jupiter.api.Test; 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.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException; import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
@ -32,6 +34,14 @@ import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException
*/ */
class EndpointMediaTypesTests { 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 @Test
void createWhenProducedIsNullShouldThrowException() { void createWhenProducedIsNullShouldThrowException() {
assertThatIllegalArgumentException().isThrownBy(() -> new EndpointMediaTypes(null, Collections.emptyList())) assertThatIllegalArgumentException().isThrownBy(() -> new EndpointMediaTypes(null, Collections.emptyList()))
@ -44,6 +54,13 @@ class EndpointMediaTypesTests {
.withMessageContaining("Consumed must not be null"); .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 @Test
void getProducedShouldReturnProduced() { void getProducedShouldReturnProduced() {
List<String> produced = Arrays.asList("a", "b", "c"); List<String> produced = Arrays.asList("a", "b", "c");

@ -17,7 +17,6 @@
package org.springframework.boot.actuate.endpoint.web.test; package org.springframework.boot.actuate.endpoint.web.test;
import java.time.Duration; import java.time.Duration;
import java.util.Arrays;
import java.util.Collection; import java.util.Collection;
import java.util.Collections; import java.util.Collections;
import java.util.HashSet; 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.TestTemplateInvocationContext;
import org.junit.jupiter.api.extension.TestTemplateInvocationContextProvider; 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.invoke.convert.ConversionServiceParameterValueMapper;
import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver; import org.springframework.boot.actuate.endpoint.web.EndpointLinksResolver;
import org.springframework.boot.actuate.endpoint.web.EndpointMapping; 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.context.annotation.Configuration;
import org.springframework.core.annotation.MergedAnnotations; import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.core.annotation.MergedAnnotations.SearchStrategy; import org.springframework.core.annotation.MergedAnnotations.SearchStrategy;
import org.springframework.http.MediaType;
import org.springframework.http.server.reactive.HttpHandler; import org.springframework.http.server.reactive.HttpHandler;
import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.util.ClassUtils; import org.springframework.util.ClassUtils;
@ -233,9 +230,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
} }
private void customize(ResourceConfig config) { private void customize(ResourceConfig config) {
List<String> mediaTypes = Arrays.asList(javax.ws.rs.core.MediaType.APPLICATION_JSON, EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
ActuatorMediaType.V2_JSON);
EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes);
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());
@ -281,8 +276,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
@Bean @Bean
WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() { WebFluxEndpointHandlerMapping webEndpointReactiveHandlerMapping() {
List<String> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes);
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());
@ -311,8 +305,7 @@ class WebEndpointTestInvocationContextProvider implements TestTemplateInvocation
@Bean @Bean
WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() { WebMvcEndpointHandlerMapping webEndpointServletHandlerMapping() {
List<String> mediaTypes = Arrays.asList(MediaType.APPLICATION_JSON_VALUE, ActuatorMediaType.V2_JSON); EndpointMediaTypes endpointMediaTypes = EndpointMediaTypes.DEFAULT;
EndpointMediaTypes endpointMediaTypes = new EndpointMediaTypes(mediaTypes, mediaTypes);
WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext, WebEndpointDiscoverer discoverer = new WebEndpointDiscoverer(this.applicationContext,
new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(), new ConversionServiceParameterValueMapper(), endpointMediaTypes, null, Collections.emptyList(),
Collections.emptyList()); Collections.emptyList());

Loading…
Cancel
Save