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;
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());

Loading…
Cancel
Save