Merge branch '6.2.x'
This commit is contained in:
commit
fed6e9b3c3
|
@ -16,6 +16,8 @@
|
|||
|
||||
package org.springframework.mock.web.server;
|
||||
|
||||
import java.security.Principal;
|
||||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
|
@ -40,15 +42,19 @@ import org.springframework.web.server.session.WebSessionManager;
|
|||
*/
|
||||
public final class MockServerWebExchange extends DefaultServerWebExchange {
|
||||
|
||||
private final Mono<Principal> principalMono;
|
||||
|
||||
|
||||
private MockServerWebExchange(
|
||||
MockServerHttpRequest request, @Nullable WebSessionManager sessionManager,
|
||||
@Nullable ApplicationContext applicationContext) {
|
||||
@Nullable ApplicationContext applicationContext, @Nullable Principal principal) {
|
||||
|
||||
super(request, new MockServerHttpResponse(),
|
||||
sessionManager != null ? sessionManager : new DefaultWebSessionManager(),
|
||||
ServerCodecConfigurer.create(), new AcceptHeaderLocaleContextResolver(),
|
||||
applicationContext);
|
||||
|
||||
this.principalMono = (principal != null) ? Mono.just(principal) : Mono.empty();
|
||||
}
|
||||
|
||||
|
||||
|
@ -57,6 +63,16 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
|
|||
return (MockServerHttpResponse) super.getResponse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the user set via {@link Builder#principal(Principal)}.
|
||||
* @since 6.2.7
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
@Override
|
||||
public <T extends Principal> Mono<T> getPrincipal() {
|
||||
return (Mono<T>) this.principalMono;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Create a {@link MockServerWebExchange} from the given mock request.
|
||||
|
@ -107,8 +123,9 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
|
|||
|
||||
private @Nullable WebSessionManager sessionManager;
|
||||
|
||||
@Nullable
|
||||
private ApplicationContext applicationContext;
|
||||
private @Nullable ApplicationContext applicationContext;
|
||||
|
||||
private @Nullable Principal principal;
|
||||
|
||||
public Builder(MockServerHttpRequest request) {
|
||||
this.request = request;
|
||||
|
@ -146,11 +163,22 @@ public final class MockServerWebExchange extends DefaultServerWebExchange {
|
|||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Provide a user to associate with the exchange.
|
||||
* @param principal the principal to use
|
||||
* @since 6.2.7
|
||||
*/
|
||||
public Builder principal(@Nullable Principal principal) {
|
||||
this.principal = principal;
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build the {@code MockServerWebExchange} instance.
|
||||
*/
|
||||
public MockServerWebExchange build() {
|
||||
return new MockServerWebExchange(this.request, this.sessionManager, this.applicationContext);
|
||||
return new MockServerWebExchange(
|
||||
this.request, this.sessionManager, this.applicationContext, this.principal);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -85,7 +85,8 @@ public final class RestClientAdapter implements HttpExchangeAdapter {
|
|||
return newRequest(values).retrieve().toEntity(bodyType);
|
||||
}
|
||||
|
||||
private RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
|
||||
@SuppressWarnings("unchecked")
|
||||
private <B> RestClient.RequestBodySpec newRequest(HttpRequestValues values) {
|
||||
|
||||
HttpMethod httpMethod = values.getHttpMethod();
|
||||
Assert.notNull(httpMethod, "HttpMethod is required");
|
||||
|
@ -127,8 +128,14 @@ public final class RestClientAdapter implements HttpExchangeAdapter {
|
|||
|
||||
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
|
||||
|
||||
if (values.getBodyValue() != null) {
|
||||
bodySpec.body(values.getBodyValue());
|
||||
B body = (B) values.getBodyValue();
|
||||
if (body != null) {
|
||||
if (values.getBodyValueType() != null) {
|
||||
bodySpec.body(body, (ParameterizedTypeReference<? super B>) values.getBodyValueType());
|
||||
}
|
||||
else {
|
||||
bodySpec.body(body);
|
||||
}
|
||||
}
|
||||
|
||||
return bodySpec;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2023 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
|
@ -86,7 +86,7 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
|
|||
return this.restTemplate.exchange(newRequest(values), bodyType);
|
||||
}
|
||||
|
||||
private RequestEntity<?> newRequest(HttpRequestValues values) {
|
||||
private <B> RequestEntity<?> newRequest(HttpRequestValues values) {
|
||||
HttpMethod httpMethod = values.getHttpMethod();
|
||||
Assert.notNull(httpMethod, "HttpMethod is required");
|
||||
|
||||
|
@ -120,11 +120,16 @@ public final class RestTemplateAdapter implements HttpExchangeAdapter {
|
|||
builder.header(HttpHeaders.COOKIE, String.join("; ", cookies));
|
||||
}
|
||||
|
||||
if (values.getBodyValue() != null) {
|
||||
return builder.body(values.getBodyValue());
|
||||
Object body = values.getBodyValue();
|
||||
if (body == null) {
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
return builder.build();
|
||||
if (values.getBodyValueType() != null) {
|
||||
return builder.body(body, values.getBodyValueType().getType());
|
||||
}
|
||||
|
||||
return builder.body(body);
|
||||
}
|
||||
|
||||
|
||||
|
|
|
@ -26,6 +26,7 @@ import java.util.Map;
|
|||
|
||||
import org.jspecify.annotations.Nullable;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
|
@ -74,6 +75,8 @@ public class HttpRequestValues {
|
|||
|
||||
private final @Nullable Object bodyValue;
|
||||
|
||||
private @Nullable ParameterizedTypeReference<?> bodyValueType;
|
||||
|
||||
|
||||
/**
|
||||
* Construct {@link HttpRequestValues}.
|
||||
|
@ -176,6 +179,14 @@ public class HttpRequestValues {
|
|||
return this.bodyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return the type for the {@linkplain #getBodyValue() body value}.
|
||||
* @since 6.2.7
|
||||
*/
|
||||
public @Nullable ParameterizedTypeReference<?> getBodyValueType() {
|
||||
return this.bodyValueType;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Return a builder for {@link HttpRequestValues}.
|
||||
|
@ -264,6 +275,8 @@ public class HttpRequestValues {
|
|||
|
||||
private @Nullable Object bodyValue;
|
||||
|
||||
private @Nullable ParameterizedTypeReference<?> bodyValueType;
|
||||
|
||||
protected Builder() {
|
||||
}
|
||||
|
||||
|
@ -417,6 +430,15 @@ public class HttpRequestValues {
|
|||
this.bodyValue = bodyValue;
|
||||
}
|
||||
|
||||
/**
|
||||
* Variant of {@link #setBodyValue(Object)} with the body type.
|
||||
* @since 6.2.7
|
||||
*/
|
||||
public void setBodyValue(@Nullable Object bodyValue, @Nullable ParameterizedTypeReference<?> valueType) {
|
||||
setBodyValue(bodyValue);
|
||||
this.bodyValueType = valueType;
|
||||
}
|
||||
|
||||
|
||||
// Implementation of {@link Metadata} methods
|
||||
|
||||
|
@ -489,9 +511,14 @@ public class HttpRequestValues {
|
|||
Map<String, Object> attributes = (this.attributes != null ?
|
||||
new HashMap<>(this.attributes) : Collections.emptyMap());
|
||||
|
||||
return createRequestValues(
|
||||
HttpRequestValues requestValues = createRequestValues(
|
||||
this.httpMethod, uri, uriBuilderFactory, uriTemplate, uriVars,
|
||||
headers, cookies, this.version, attributes, bodyValue);
|
||||
|
||||
// In 6.2.x only, temporarily work around protected methods
|
||||
requestValues.bodyValueType = this.bodyValueType;
|
||||
|
||||
return requestValues;
|
||||
}
|
||||
|
||||
protected boolean hasParts() {
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
|
@ -83,15 +83,16 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
|
|||
if (this.reactiveAdapterRegistry != null) {
|
||||
ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(parameter.getParameterType());
|
||||
if (adapter != null) {
|
||||
MethodParameter nestedParameter = parameter.nested();
|
||||
MethodParameter nestedParam = parameter.nested();
|
||||
|
||||
String message = "Async type for @RequestBody should produce value(s)";
|
||||
Assert.isTrue(!adapter.isNoValue(), message);
|
||||
Assert.isTrue(nestedParameter.getNestedParameterType() != Void.class, message);
|
||||
Assert.isTrue(nestedParam.getNestedParameterType() != Void.class, message);
|
||||
|
||||
if (requestValues instanceof ReactiveHttpRequestValues.Builder reactiveRequestValues) {
|
||||
reactiveRequestValues.setBodyPublisher(
|
||||
adapter.toPublisher(argument), asParameterizedTypeRef(nestedParameter));
|
||||
if (requestValues instanceof ReactiveHttpRequestValues.Builder rrv) {
|
||||
rrv.setBodyPublisher(
|
||||
adapter.toPublisher(argument),
|
||||
ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType()));
|
||||
}
|
||||
else {
|
||||
throw new IllegalStateException(
|
||||
|
@ -103,12 +104,8 @@ public class RequestBodyArgumentResolver implements HttpServiceArgumentResolver
|
|||
}
|
||||
|
||||
// Not a reactive type
|
||||
requestValues.setBodyValue(argument);
|
||||
requestValues.setBodyValue(argument, ParameterizedTypeReference.forType(parameter.getGenericParameterType()));
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ParameterizedTypeReference<Object> asParameterizedTypeRef(MethodParameter nestedParam) {
|
||||
return ParameterizedTypeReference.forType(nestedParam.getNestedGenericParameterType());
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -22,7 +22,9 @@ import java.lang.annotation.Retention;
|
|||
import java.lang.annotation.RetentionPolicy;
|
||||
import java.lang.annotation.Target;
|
||||
import java.net.URI;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Optional;
|
||||
import java.util.Set;
|
||||
import java.util.function.BiFunction;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
|
@ -284,6 +286,19 @@ class RestClientAdapterTests {
|
|||
assertThat(request.getHeader("X-API-Version")).isEqualTo("1.2");
|
||||
}
|
||||
|
||||
@ParameterizedAdapterTest // gh-34793
|
||||
void postSet(MockWebServer server, Service service) throws InterruptedException {
|
||||
Set<Person> persons = new LinkedHashSet<>();
|
||||
persons.add(new Person("John"));
|
||||
persons.add(new Person("Richard"));
|
||||
service.postPersonSet(persons);
|
||||
|
||||
RecordedRequest request = server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
assertThat(request.getPath()).isEqualTo("/persons");
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]");
|
||||
}
|
||||
|
||||
|
||||
private static MockWebServer anotherServer() {
|
||||
MockWebServer server = new MockWebServer();
|
||||
|
@ -317,6 +332,9 @@ class RestClientAdapterTests {
|
|||
@PostExchange
|
||||
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
|
||||
|
||||
@PostExchange(url = "/persons", contentType = MediaType.APPLICATION_JSON_VALUE)
|
||||
void postPersonSet(@RequestBody Set<Person> set);
|
||||
|
||||
@PutExchange
|
||||
void putWithCookies(@CookieValue String firstCookie, @CookieValue String secondCookie);
|
||||
|
||||
|
@ -335,4 +353,19 @@ class RestClientAdapterTests {
|
|||
ResponseEntity<String> getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory);
|
||||
}
|
||||
|
||||
|
||||
static final class Person {
|
||||
|
||||
private final String name;
|
||||
|
||||
Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
/*
|
||||
* Copyright 2002-2024 the original author or authors.
|
||||
* Copyright 2002-2025 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.
|
||||
|
@ -54,6 +54,7 @@ class RequestBodyArgumentResolverTests {
|
|||
this.service.execute(body);
|
||||
|
||||
assertThat(getBodyValue()).isEqualTo(body);
|
||||
assertThat(getBodyValueType()).isEqualTo(new ParameterizedTypeReference<String>() {});
|
||||
assertThat(getPublisherBody()).isNull();
|
||||
}
|
||||
|
||||
|
@ -172,6 +173,10 @@ class RequestBodyArgumentResolverTests {
|
|||
return getReactiveRequestValues().getBodyValue();
|
||||
}
|
||||
|
||||
private @Nullable ParameterizedTypeReference<?> getBodyValueType() {
|
||||
return getReactiveRequestValues().getBodyValueType();
|
||||
}
|
||||
|
||||
private @Nullable Publisher<?> getPublisherBody() {
|
||||
return getReactiveRequestValues().getBodyPublisher();
|
||||
}
|
||||
|
|
|
@ -98,8 +98,8 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
|
|||
return newRequest(requestValues).retrieve().toEntityFlux(bodyType);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ReactiveStreamsUnusedPublisher")
|
||||
private WebClient.RequestBodySpec newRequest(HttpRequestValues values) {
|
||||
@SuppressWarnings({"ReactiveStreamsUnusedPublisher", "unchecked"})
|
||||
private <B> WebClient.RequestBodySpec newRequest(HttpRequestValues values) {
|
||||
|
||||
HttpMethod httpMethod = values.getHttpMethod();
|
||||
Assert.notNull(httpMethod, "HttpMethod is required");
|
||||
|
@ -135,12 +135,18 @@ public final class WebClientAdapter extends AbstractReactorHttpExchangeAdapter {
|
|||
bodySpec.attributes(attributes -> attributes.putAll(values.getAttributes()));
|
||||
|
||||
if (values.getBodyValue() != null) {
|
||||
bodySpec.bodyValue(values.getBodyValue());
|
||||
if (values.getBodyValueType() != null) {
|
||||
B body = (B) values.getBodyValue();
|
||||
bodySpec.bodyValue(body, (ParameterizedTypeReference<B>) values.getBodyValueType());
|
||||
}
|
||||
else {
|
||||
bodySpec.bodyValue(values.getBodyValue());
|
||||
}
|
||||
}
|
||||
else if (values instanceof ReactiveHttpRequestValues reactiveRequestValues) {
|
||||
Publisher<?> body = reactiveRequestValues.getBodyPublisher();
|
||||
else if (values instanceof ReactiveHttpRequestValues rhrv) {
|
||||
Publisher<?> body = rhrv.getBodyPublisher();
|
||||
if (body != null) {
|
||||
ParameterizedTypeReference<?> elementType = reactiveRequestValues.getBodyPublisherElementType();
|
||||
ParameterizedTypeReference<?> elementType = rhrv.getBodyPublisherElementType();
|
||||
Assert.notNull(elementType, "Publisher body element type is required");
|
||||
bodySpec.body(body, elementType);
|
||||
}
|
||||
|
|
|
@ -21,7 +21,9 @@ import java.io.IOException;
|
|||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.function.Consumer;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
|
@ -39,6 +41,7 @@ import org.springframework.util.LinkedMultiValueMap;
|
|||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
@ -166,6 +169,22 @@ class WebClientAdapterTests {
|
|||
"Content-Type: text/plain;charset=UTF-8", "Content-Length: 5", "test2");
|
||||
}
|
||||
|
||||
@Test // gh-34793
|
||||
void postSet() throws InterruptedException {
|
||||
prepareResponse(response -> response.setResponseCode(201));
|
||||
|
||||
Set<Person> persons = new LinkedHashSet<>();
|
||||
persons.add(new Person("John"));
|
||||
persons.add(new Person("Richard"));
|
||||
|
||||
initService().postPersonSet(persons);
|
||||
|
||||
RecordedRequest request = server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
assertThat(request.getPath()).isEqualTo("/persons");
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("[{\"name\":\"John\"},{\"name\":\"Richard\"}]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void uriBuilderFactory() throws Exception {
|
||||
String ignoredResponseBody = "hello";
|
||||
|
@ -249,6 +268,9 @@ class WebClientAdapterTests {
|
|||
@PostExchange
|
||||
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
|
||||
|
||||
@PostExchange("/persons")
|
||||
void postPersonSet(@RequestBody Set<Person> set);
|
||||
|
||||
@GetExchange("/greeting")
|
||||
String getWithUriBuilderFactory(UriBuilderFactory uriBuilderFactory);
|
||||
|
||||
|
@ -260,4 +282,19 @@ class WebClientAdapterTests {
|
|||
String getWithIgnoredUriBuilderFactory(URI uri, UriBuilderFactory uriBuilderFactory);
|
||||
}
|
||||
|
||||
|
||||
static final class Person {
|
||||
|
||||
private final String name;
|
||||
|
||||
Person(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue