Add RestTemplate support for HTTP interface client
See gh-30117
This commit is contained in:
committed by
rstoyanchev
parent
bf82ed7186
commit
268f3c853e
@@ -0,0 +1,220 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.client.support;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.Optional;
|
||||
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
import org.springframework.web.bind.annotation.CookieValue;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RequestPart;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
import org.springframework.web.service.annotation.PostExchange;
|
||||
import org.springframework.web.service.annotation.PutExchange;
|
||||
import org.springframework.web.service.invoker.HttpServiceProxyFactory;
|
||||
import org.springframework.web.testfixture.servlet.MockMultipartFile;
|
||||
import org.springframework.web.util.DefaultUriBuilderFactory;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link HttpServiceProxyFactory HTTP Service proxy} using
|
||||
* {@link RestTemplate} and {@link MockWebServer}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class RestTemplateHttpServiceProxyTests {
|
||||
|
||||
private MockWebServer server;
|
||||
|
||||
private TestService testService;
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.server = new MockWebServer();
|
||||
prepareResponse();
|
||||
this.testService = initTestService();
|
||||
}
|
||||
|
||||
private TestService initTestService() {
|
||||
RestTemplate restTemplate = new RestTemplate();
|
||||
restTemplate.setUriTemplateHandler(new DefaultUriBuilderFactory(this.server.url("/").toString()));
|
||||
return HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate))
|
||||
.build()
|
||||
.createClient(TestService.class);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
@AfterEach
|
||||
void shutDown() throws IOException {
|
||||
if (this.server != null) {
|
||||
this.server.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRequest() throws InterruptedException {
|
||||
String response = testService.getRequest();
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(response).isEqualTo("Hello Spring!");
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/test");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRequestWithPathVariable() throws InterruptedException {
|
||||
ResponseEntity<String> response = testService.getRequestWithPathVariable("456");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isEqualTo("Hello Spring!");
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getPath()).isEqualTo("/test/456");
|
||||
}
|
||||
|
||||
@Test
|
||||
void getRequestWithDynamicUri() throws InterruptedException {
|
||||
URI dynamicUri = this.server.url("/greeting/123").uri();
|
||||
|
||||
Optional<String> response = testService.getRequestWithDynamicUri(dynamicUri, "456");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(response.orElse("empty")).isEqualTo("Hello Spring!");
|
||||
assertThat(request.getMethod()).isEqualTo("GET");
|
||||
assertThat(request.getRequestUrl().uri()).isEqualTo(dynamicUri);
|
||||
}
|
||||
|
||||
@Test
|
||||
void postWithRequestHeader() throws InterruptedException {
|
||||
testService.postRequestWithHeader("testHeader", "testBody");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("POST");
|
||||
assertThat(request.getPath()).isEqualTo("/test");
|
||||
assertThat(request.getHeaders().get("testHeaderName")).isEqualTo("testHeader");
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("testBody");
|
||||
}
|
||||
|
||||
@Test
|
||||
void formData() throws Exception {
|
||||
MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
|
||||
map.add("param1", "value 1");
|
||||
map.add("param2", "value 2");
|
||||
|
||||
testService.postForm(map);
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeaders().get("Content-Type"))
|
||||
.isEqualTo("application/x-www-form-urlencoded;charset=UTF-8");
|
||||
assertThat(request.getBody().readUtf8()).isEqualTo("param1=value+1¶m2=value+2");
|
||||
}
|
||||
|
||||
@Test // gh-30342
|
||||
void multipart() throws InterruptedException {
|
||||
String fileName = "testFileName";
|
||||
String originalFileName = "originalTestFileName";
|
||||
MultipartFile file = new MockMultipartFile(fileName, originalFileName, MediaType.APPLICATION_JSON_VALUE,
|
||||
"test".getBytes());
|
||||
|
||||
testService.postMultipart(file, "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeaders().get("Content-Type")).startsWith("multipart/form-data;boundary=");
|
||||
assertThat(request.getBody().readUtf8()).containsSubsequence(
|
||||
"Content-Disposition: form-data; name=\"file\"; filename=\"originalTestFileName\"",
|
||||
"Content-Type: application/json", "Content-Length: 4", "test",
|
||||
"Content-Disposition: form-data; name=\"anotherPart\"", "Content-Type: text/plain;charset=UTF-8",
|
||||
"Content-Length: 5", "test2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void putRequestWithCookies() throws InterruptedException {
|
||||
testService.putRequestWithCookies("test1", "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("PUT");
|
||||
assertThat(request.getHeader("Cookie")).isEqualTo("firstCookie=test1; secondCookie=test2");
|
||||
}
|
||||
|
||||
@Test
|
||||
void putRequestWithSameNameCookies() throws InterruptedException {
|
||||
testService.putRequestWithSameNameCookies("test1", "test2");
|
||||
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getMethod()).isEqualTo("PUT");
|
||||
assertThat(request.getHeader("Cookie")).isEqualTo("testCookie=test1; testCookie=test2");
|
||||
}
|
||||
|
||||
private void prepareResponse() {
|
||||
MockResponse response = new MockResponse();
|
||||
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!");
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
private interface TestService {
|
||||
|
||||
@GetExchange("/test")
|
||||
String getRequest();
|
||||
|
||||
@GetExchange("/test/{id}")
|
||||
ResponseEntity<String> getRequestWithPathVariable(@PathVariable String id);
|
||||
|
||||
@GetExchange("/test/{id}")
|
||||
Optional<String> getRequestWithDynamicUri(@Nullable URI uri, @PathVariable String id);
|
||||
|
||||
@PostExchange("/test")
|
||||
void postRequestWithHeader(@RequestHeader("testHeaderName") String testHeader,
|
||||
@RequestBody String requestBody);
|
||||
|
||||
@PostExchange(contentType = "application/x-www-form-urlencoded")
|
||||
void postForm(@RequestParam MultiValueMap<String, String> params);
|
||||
|
||||
@PostExchange
|
||||
void postMultipart(MultipartFile file, @RequestPart String anotherPart);
|
||||
|
||||
@PutExchange
|
||||
void putRequestWithCookies(@CookieValue String firstCookie,
|
||||
@CookieValue String secondCookie);
|
||||
|
||||
@PutExchange
|
||||
void putRequestWithSameNameCookies(@CookieValue("testCookie") String firstCookie,
|
||||
@CookieValue("testCookie") String secondCookie);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.service.invoker;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter} that
|
||||
* stubs the client invocations.
|
||||
* <p>
|
||||
* The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
|
||||
* {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
|
||||
* typed interface without the need for class casts.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class HttpClientServiceMethodTests extends ReactiveHttpServiceMethodTests {
|
||||
|
||||
@BeforeEach
|
||||
void setUp(){
|
||||
this.client = new TestHttpClientAdapter();
|
||||
this.proxyFactory = HttpServiceProxyFactory.builder((HttpClientAdapter) this.client).build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.service.invoker;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceMethod} with a blocking test {@link TestHttpExchangeAdapter} that
|
||||
* stubs the client invocations.
|
||||
* <p>
|
||||
* The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
|
||||
* {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
|
||||
* typed interface without the need for class casts.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class HttpExchangeAdapterServiceMethodTests extends HttpServiceMethodTests {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.client = new TestHttpExchangeAdapter();
|
||||
this.proxyFactory = HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter((HttpExchangeAdapter) this.client)
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -16,18 +16,12 @@
|
||||
|
||||
package org.springframework.web.service.invoker;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -42,84 +36,25 @@ import static org.springframework.http.MediaType.APPLICATION_CBOR_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
|
||||
* that stubs the client invocations.
|
||||
* Base class for testing {@link HttpServiceMethod} with a test {@link TestHttpClientAdapter}
|
||||
* and a test {@link TestHttpExchangeAdapter} that stub the client invocations.
|
||||
*
|
||||
* <p>The tests do not create or invoke {@code HttpServiceMethod} directly but
|
||||
* rather use {@link HttpServiceProxyFactory} to create a service proxy in order to
|
||||
* use a strongly typed interface without the need for class casts.
|
||||
* <p>
|
||||
* The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
|
||||
* {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
|
||||
* typed interface without the need for class casts.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class HttpServiceMethodTests {
|
||||
abstract class HttpServiceMethodTests {
|
||||
|
||||
private static final ParameterizedTypeReference<String> BODY_TYPE = new ParameterizedTypeReference<>() {};
|
||||
protected static final ParameterizedTypeReference<String> BODY_TYPE = new ParameterizedTypeReference<>() {
|
||||
};
|
||||
|
||||
private final TestHttpClientAdapter client = new TestHttpClientAdapter();
|
||||
protected TestAdapter client;
|
||||
|
||||
private final HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.client).build();
|
||||
|
||||
|
||||
@Test
|
||||
void reactorService() {
|
||||
ReactorService service = this.proxyFactory.createClient(ReactorService.class);
|
||||
|
||||
Mono<Void> voidMono = service.execute();
|
||||
StepVerifier.create(voidMono).verifyComplete();
|
||||
verifyClientInvocation("requestToVoid", null);
|
||||
|
||||
Mono<HttpHeaders> headersMono = service.getHeaders();
|
||||
StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
|
||||
verifyClientInvocation("requestToHeaders", null);
|
||||
|
||||
Mono<String> body = service.getBody();
|
||||
StepVerifier.create(body).expectNext("requestToBody").verifyComplete();
|
||||
verifyClientInvocation("requestToBody", BODY_TYPE);
|
||||
|
||||
Flux<String> fluxBody = service.getFluxBody();
|
||||
StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
|
||||
verifyClientInvocation("requestToBodyFlux", BODY_TYPE);
|
||||
|
||||
Mono<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||
StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
|
||||
verifyClientInvocation("requestToBodilessEntity", null);
|
||||
|
||||
Mono<ResponseEntity<String>> entity = service.getEntity();
|
||||
StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
|
||||
verifyClientInvocation("requestToEntity", BODY_TYPE);
|
||||
|
||||
Mono<ResponseEntity<Flux<String>>> fluxEntity= service.getFluxEntity();
|
||||
StepVerifier.create(fluxEntity.flatMapMany(HttpEntity::getBody)).expectNext("request", "To", "Entity", "Flux").verifyComplete();
|
||||
verifyClientInvocation("requestToEntityFlux", BODY_TYPE);
|
||||
|
||||
assertThat(service.getDefaultMethodValue()).isEqualTo("default value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rxJavaService() {
|
||||
RxJavaService service = this.proxyFactory.createClient(RxJavaService.class);
|
||||
Completable completable = service.execute();
|
||||
assertThat(completable).isNotNull();
|
||||
|
||||
Single<HttpHeaders> headersSingle = service.getHeaders();
|
||||
assertThat(headersSingle.blockingGet()).isNotNull();
|
||||
|
||||
Single<String> bodySingle = service.getBody();
|
||||
assertThat(bodySingle.blockingGet()).isEqualTo("requestToBody");
|
||||
|
||||
Flowable<String> bodyFlow = service.getFlowableBody();
|
||||
assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
|
||||
|
||||
Single<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||
assertThat(voidEntity.blockingGet().getBody()).isNull();
|
||||
|
||||
Single<ResponseEntity<String>> entitySingle = service.getEntity();
|
||||
assertThat(entitySingle.blockingGet().getBody()).isEqualTo("requestToEntity");
|
||||
|
||||
Single<ResponseEntity<Flowable<String>>> entityFlow = service.getFlowableEntity();
|
||||
Flowable<String> body = (entityFlow.blockingGet()).getBody();
|
||||
assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
|
||||
}
|
||||
protected HttpServiceProxyFactory proxyFactory;
|
||||
|
||||
@Test
|
||||
void blockingService() {
|
||||
@@ -131,16 +66,19 @@ class HttpServiceMethodTests {
|
||||
assertThat(headers).isNotNull();
|
||||
|
||||
String body = service.getBody();
|
||||
assertThat(body).isEqualTo("requestToBody");
|
||||
assertThat(body).isEqualTo(client.getInvokedMethodReference());
|
||||
|
||||
Optional<String> optional = service.getBodyOptional();
|
||||
assertThat(optional).contains("requestToBody");
|
||||
assertThat(optional).contains("body");
|
||||
|
||||
ResponseEntity<String> entity = service.getEntity();
|
||||
assertThat(entity.getBody()).isEqualTo("requestToEntity");
|
||||
assertThat(entity.getBody()).isEqualTo("entity");
|
||||
|
||||
ResponseEntity<Void> voidEntity = service.getVoidEntity();
|
||||
assertThat(voidEntity.getBody()).isNull();
|
||||
|
||||
List<String> list = service.getList();
|
||||
assertThat(list.get(0)).isEqualTo("body");
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -166,9 +104,12 @@ class HttpServiceMethodTests {
|
||||
|
||||
@Test
|
||||
void typeAndMethodAnnotatedService() {
|
||||
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder(this.client)
|
||||
.embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
|
||||
.build();
|
||||
HttpExchangeAdapter actualClient = this.client instanceof HttpClientAdapter httpClient
|
||||
? httpClient.asHttpExchangeAdapter() : (HttpExchangeAdapter) client;
|
||||
HttpServiceProxyFactory proxyFactory = HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(actualClient)
|
||||
.embeddedValueResolver(value -> (value.equals("${baseUrl}") ? "/base" : value))
|
||||
.build();
|
||||
|
||||
MethodLevelAnnotatedService service = proxyFactory.createClient(TypeAndMethodLevelAnnotatedService.class);
|
||||
|
||||
@@ -189,68 +130,11 @@ class HttpServiceMethodTests {
|
||||
assertThat(requestValues.getHeaders().getAccept()).containsExactly(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
private void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
|
||||
assertThat(this.client.getInvokedMethodName()).isEqualTo(methodName);
|
||||
protected void verifyClientInvocation(String methodName, @Nullable ParameterizedTypeReference<?> expectedBodyType) {
|
||||
assertThat(this.client.getInvokedMethodReference()).isEqualTo(methodName);
|
||||
assertThat(this.client.getBodyType()).isEqualTo(expectedBodyType);
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface ReactorService {
|
||||
|
||||
@GetExchange
|
||||
Mono<Void> execute();
|
||||
|
||||
@GetExchange
|
||||
Mono<HttpHeaders> getHeaders();
|
||||
|
||||
@GetExchange
|
||||
Mono<String> getBody();
|
||||
|
||||
@GetExchange
|
||||
Flux<String> getFluxBody();
|
||||
|
||||
@GetExchange
|
||||
Mono<ResponseEntity<Void>> getVoidEntity();
|
||||
|
||||
@GetExchange
|
||||
Mono<ResponseEntity<String>> getEntity();
|
||||
|
||||
@GetExchange
|
||||
Mono<ResponseEntity<Flux<String>>> getFluxEntity();
|
||||
|
||||
default String getDefaultMethodValue() {
|
||||
return "default value";
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface RxJavaService {
|
||||
|
||||
@GetExchange
|
||||
Completable execute();
|
||||
|
||||
@GetExchange
|
||||
Single<HttpHeaders> getHeaders();
|
||||
|
||||
@GetExchange
|
||||
Single<String> getBody();
|
||||
|
||||
@GetExchange
|
||||
Flowable<String> getFlowableBody();
|
||||
|
||||
@GetExchange
|
||||
Single<ResponseEntity<Void>> getVoidEntity();
|
||||
|
||||
@GetExchange
|
||||
Single<ResponseEntity<String>> getEntity();
|
||||
|
||||
@GetExchange
|
||||
Single<ResponseEntity<Flowable<String>>> getFlowableEntity();
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface BlockingService {
|
||||
|
||||
@@ -271,8 +155,11 @@ class HttpServiceMethodTests {
|
||||
|
||||
@GetExchange
|
||||
ResponseEntity<String> getEntity();
|
||||
}
|
||||
|
||||
@GetExchange
|
||||
List<String> getList();
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface MethodLevelAnnotatedService {
|
||||
@@ -285,10 +172,10 @@ class HttpServiceMethodTests {
|
||||
|
||||
}
|
||||
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@HttpExchange(url = "${baseUrl}", contentType = APPLICATION_CBOR_VALUE, accept = APPLICATION_CBOR_VALUE)
|
||||
private interface TypeAndMethodLevelAnnotatedService extends MethodLevelAnnotatedService {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,167 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.service.invoker;
|
||||
|
||||
import io.reactivex.rxjava3.core.Completable;
|
||||
import io.reactivex.rxjava3.core.Flowable;
|
||||
import io.reactivex.rxjava3.core.Single;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import reactor.test.StepVerifier;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.service.annotation.GetExchange;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Base class for testing reactive scenarios in {@link HttpServiceMethod} with
|
||||
* a test {@link TestHttpClientAdapter} and a test {@link TestHttpExchangeAdapter}
|
||||
* that stub the client invocations.
|
||||
*
|
||||
* <p>
|
||||
* The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
|
||||
* {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
|
||||
* typed interface without the need for class casts.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
abstract class ReactiveHttpServiceMethodTests extends HttpServiceMethodTests {
|
||||
|
||||
@Test
|
||||
void reactorService() {
|
||||
ReactorService service = proxyFactory.createClient(ReactorService.class);
|
||||
|
||||
Mono<Void> voidMono = service.execute();
|
||||
StepVerifier.create(voidMono).verifyComplete();
|
||||
verifyClientInvocation("void", null);
|
||||
|
||||
Mono<HttpHeaders> headersMono = service.getHeaders();
|
||||
StepVerifier.create(headersMono).expectNextCount(1).verifyComplete();
|
||||
verifyClientInvocation("headers", null);
|
||||
|
||||
Mono<String> body = service.getBody();
|
||||
StepVerifier.create(body).expectNext("body").verifyComplete();
|
||||
verifyClientInvocation("body", BODY_TYPE);
|
||||
|
||||
Flux<String> fluxBody = service.getFluxBody();
|
||||
StepVerifier.create(fluxBody).expectNext("request", "To", "Body", "Flux").verifyComplete();
|
||||
verifyClientInvocation("bodyFlux", BODY_TYPE);
|
||||
|
||||
Mono<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||
StepVerifier.create(voidEntity).expectNext(ResponseEntity.ok().build()).verifyComplete();
|
||||
verifyClientInvocation("bodilessEntity", null);
|
||||
|
||||
Mono<ResponseEntity<String>> entity = service.getEntity();
|
||||
StepVerifier.create(entity).expectNext(ResponseEntity.ok("requestToEntity"));
|
||||
verifyClientInvocation("entity", BODY_TYPE);
|
||||
|
||||
Mono<ResponseEntity<Flux<String>>> fluxEntity = service.getFluxEntity();
|
||||
StepVerifier.create(fluxEntity.flatMapMany(HttpEntity::getBody))
|
||||
.expectNext("request", "To", "Entity", "Flux")
|
||||
.verifyComplete();
|
||||
verifyClientInvocation("entityFlux", BODY_TYPE);
|
||||
|
||||
assertThat(service.getDefaultMethodValue()).isEqualTo("default value");
|
||||
}
|
||||
|
||||
@Test
|
||||
void rxJavaService() {
|
||||
RxJavaService service = this.proxyFactory.createClient(RxJavaService.class);
|
||||
Completable completable = service.execute();
|
||||
assertThat(completable).isNotNull();
|
||||
|
||||
Single<HttpHeaders> headersSingle = service.getHeaders();
|
||||
assertThat(headersSingle.blockingGet()).isNotNull();
|
||||
|
||||
Single<String> bodySingle = service.getBody();
|
||||
assertThat(bodySingle.blockingGet()).isEqualTo("body");
|
||||
|
||||
Flowable<String> bodyFlow = service.getFlowableBody();
|
||||
assertThat(bodyFlow.toList().blockingGet()).asList().containsExactly("request", "To", "Body", "Flux");
|
||||
|
||||
Single<ResponseEntity<Void>> voidEntity = service.getVoidEntity();
|
||||
assertThat(voidEntity.blockingGet().getBody()).isNull();
|
||||
|
||||
Single<ResponseEntity<String>> entitySingle = service.getEntity();
|
||||
assertThat(entitySingle.blockingGet().getBody()).isEqualTo("entity");
|
||||
|
||||
Single<ResponseEntity<Flowable<String>>> entityFlow = service.getFlowableEntity();
|
||||
Flowable<String> body = (entityFlow.blockingGet()).getBody();
|
||||
assertThat(body.toList().blockingGet()).containsExactly("request", "To", "Entity", "Flux");
|
||||
}
|
||||
|
||||
private interface ReactorService {
|
||||
|
||||
@GetExchange
|
||||
Mono<Void> execute();
|
||||
|
||||
@GetExchange
|
||||
Mono<HttpHeaders> getHeaders();
|
||||
|
||||
@GetExchange
|
||||
Mono<String> getBody();
|
||||
|
||||
@GetExchange
|
||||
Flux<String> getFluxBody();
|
||||
|
||||
@GetExchange
|
||||
Mono<ResponseEntity<Void>> getVoidEntity();
|
||||
|
||||
@GetExchange
|
||||
Mono<ResponseEntity<String>> getEntity();
|
||||
|
||||
@GetExchange
|
||||
Mono<ResponseEntity<Flux<String>>> getFluxEntity();
|
||||
|
||||
default String getDefaultMethodValue() {
|
||||
return "default value";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
private interface RxJavaService {
|
||||
|
||||
@GetExchange
|
||||
Completable execute();
|
||||
|
||||
@GetExchange
|
||||
Single<HttpHeaders> getHeaders();
|
||||
|
||||
@GetExchange
|
||||
Single<String> getBody();
|
||||
|
||||
@GetExchange
|
||||
Flowable<String> getFlowableBody();
|
||||
|
||||
@GetExchange
|
||||
Single<ResponseEntity<Void>> getVoidEntity();
|
||||
|
||||
@GetExchange
|
||||
Single<ResponseEntity<String>> getEntity();
|
||||
|
||||
@GetExchange
|
||||
Single<ResponseEntity<Flowable<String>>> getFlowableEntity();
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.service.invoker;
|
||||
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
|
||||
/**
|
||||
* Tests for {@link HttpServiceMethod} with an {@link HttpExchangeAdapter}
|
||||
* build from a test {@link TestHttpClientAdapter} that stubs the client invocations.
|
||||
* <p>
|
||||
* The tests do not create or invoke {@code HttpServiceMethod} directly but rather use
|
||||
* {@link HttpServiceProxyFactory} to create a service proxy in order to use a strongly
|
||||
* typed interface without the need for class casts.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class ReactorExchangeAdapterHttpServiceMethodTests extends ReactiveHttpServiceMethodTests {
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
this.client = new TestHttpClientAdapter();
|
||||
this.proxyFactory = HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(((HttpClientAdapter) this.client).asHttpExchangeAdapter())
|
||||
.build();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.service.invoker;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
/**
|
||||
* A helper interface for verifying method invoked on {@link HttpExchangeAdapter}
|
||||
* and {@link HttpClientAdapter}, as well as their values.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
interface TestAdapter {
|
||||
|
||||
String getInvokedMethodReference();
|
||||
|
||||
HttpRequestValues getRequestValues();
|
||||
|
||||
@Nullable
|
||||
ParameterizedTypeReference<?> getBodyType();
|
||||
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2002-2022 the original author or authors.
|
||||
* Copyright 2002-2023 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.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.web.service.invoker;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
@@ -33,10 +35,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
class TestHttpClientAdapter implements HttpClientAdapter {
|
||||
class TestHttpClientAdapter implements HttpClientAdapter, TestAdapter {
|
||||
|
||||
@Nullable
|
||||
private String invokedMethodName;
|
||||
private String invokedForReturnMethodReference;
|
||||
|
||||
@Nullable
|
||||
private HttpRequestValues requestValues;
|
||||
@@ -44,17 +46,19 @@ class TestHttpClientAdapter implements HttpClientAdapter {
|
||||
@Nullable
|
||||
private ParameterizedTypeReference<?> bodyType;
|
||||
|
||||
|
||||
public String getInvokedMethodName() {
|
||||
assertThat(this.invokedMethodName).isNotNull();
|
||||
return this.invokedMethodName;
|
||||
@Override
|
||||
public String getInvokedMethodReference() {
|
||||
assertThat(this.invokedForReturnMethodReference).isNotNull();
|
||||
return this.invokedForReturnMethodReference;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpRequestValues getRequestValues() {
|
||||
assertThat(this.requestValues).isNotNull();
|
||||
return this.requestValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ParameterizedTypeReference<?> getBodyType() {
|
||||
return this.bodyType;
|
||||
@@ -65,31 +69,33 @@ class TestHttpClientAdapter implements HttpClientAdapter {
|
||||
|
||||
@Override
|
||||
public Mono<Void> requestToVoid(HttpRequestValues requestValues) {
|
||||
saveInput("requestToVoid", requestValues, null);
|
||||
saveInput("void", requestValues, null);
|
||||
return Mono.empty();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<HttpHeaders> requestToHeaders(HttpRequestValues requestValues) {
|
||||
saveInput("requestToHeaders", requestValues, null);
|
||||
saveInput("headers", requestValues, null);
|
||||
return Mono.just(new HttpHeaders());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<T> requestToBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("requestToBody", requestValues, bodyType);
|
||||
return (Mono<T>) Mono.just(getInvokedMethodName());
|
||||
saveInput("body", requestValues, bodyType);
|
||||
return bodyType.getType().getTypeName().contains("List") ?
|
||||
(Mono<T>) Mono.just(Collections.singletonList(getInvokedMethodReference()))
|
||||
: (Mono<T>) Mono.just(getInvokedMethodReference());
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Flux<T> requestToBodyFlux(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("requestToBodyFlux", requestValues, bodyType);
|
||||
saveInput("bodyFlux", requestValues, bodyType);
|
||||
return (Flux<T>) Flux.just("request", "To", "Body", "Flux");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Mono<ResponseEntity<Void>> requestToBodilessEntity(HttpRequestValues requestValues) {
|
||||
saveInput("requestToBodilessEntity", requestValues, null);
|
||||
saveInput("bodilessEntity", requestValues, null);
|
||||
return Mono.just(ResponseEntity.ok().build());
|
||||
}
|
||||
|
||||
@@ -97,22 +103,22 @@ class TestHttpClientAdapter implements HttpClientAdapter {
|
||||
public <T> Mono<ResponseEntity<T>> requestToEntity(
|
||||
HttpRequestValues requestValues, ParameterizedTypeReference<T> type) {
|
||||
|
||||
saveInput("requestToEntity", requestValues, type);
|
||||
return Mono.just((ResponseEntity<T>) ResponseEntity.ok("requestToEntity"));
|
||||
saveInput("entity", requestValues, type);
|
||||
return Mono.just((ResponseEntity<T>) ResponseEntity.ok("entity"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> Mono<ResponseEntity<Flux<T>>> requestToEntityFlux(
|
||||
HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||
|
||||
saveInput("requestToEntityFlux", requestValues, bodyType);
|
||||
saveInput("entityFlux", requestValues, bodyType);
|
||||
return Mono.just(ResponseEntity.ok((Flux<T>) Flux.just("request", "To", "Entity", "Flux")));
|
||||
}
|
||||
|
||||
private <T> void saveInput(
|
||||
String methodName, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference<T> bodyType) {
|
||||
String reference, HttpRequestValues requestValues, @Nullable ParameterizedTypeReference<T> bodyType) {
|
||||
|
||||
this.invokedMethodName = methodName;
|
||||
this.invokedForReturnMethodReference = reference;
|
||||
this.requestValues = requestValues;
|
||||
this.bodyType = bodyType;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright 2002-2023 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.web.service.invoker;
|
||||
|
||||
import java.util.Collections;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* {@link HttpExchangeAdapter} with stubbed responses.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public class TestHttpExchangeAdapter implements HttpExchangeAdapter, TestAdapter {
|
||||
|
||||
@Nullable
|
||||
private String invokedMethodName;
|
||||
|
||||
@Nullable
|
||||
private HttpRequestValues requestValues;
|
||||
|
||||
@Nullable
|
||||
private ParameterizedTypeReference<?> bodyType;
|
||||
|
||||
public String getInvokedMethodReference() {
|
||||
assertThat(this.invokedMethodName).isNotNull();
|
||||
return this.invokedMethodName;
|
||||
}
|
||||
|
||||
public HttpRequestValues getRequestValues() {
|
||||
assertThat(this.requestValues).isNotNull();
|
||||
return this.requestValues;
|
||||
}
|
||||
|
||||
@Override
|
||||
@Nullable
|
||||
public ParameterizedTypeReference<?> getBodyType() {
|
||||
return this.bodyType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Void exchange(HttpRequestValues requestValues) {
|
||||
saveInput("void", requestValues, null);
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders exchangeForHeaders(HttpRequestValues requestValues) {
|
||||
saveInput("headers", requestValues, null);
|
||||
return new HttpHeaders();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T exchangeForBody(HttpRequestValues requestValues, ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("body", requestValues, bodyType);
|
||||
return bodyType.getType().getTypeName().contains("List")
|
||||
? (T) Collections.singletonList(getInvokedMethodReference()) : (T) getInvokedMethodReference();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<Void> exchangeForBodilessEntity(HttpRequestValues requestValues) {
|
||||
saveInput("bodilessEntity", requestValues, null);
|
||||
return ResponseEntity.ok().build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> ResponseEntity<T> exchangeForEntity(HttpRequestValues requestValues,
|
||||
ParameterizedTypeReference<T> bodyType) {
|
||||
saveInput("entity", requestValues, bodyType);
|
||||
return (ResponseEntity<T>) ResponseEntity.ok(this.getInvokedMethodReference());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRequestAttributes() {
|
||||
return false;
|
||||
}
|
||||
|
||||
private <T> void saveInput(String methodName, HttpRequestValues requestValues,
|
||||
@Nullable ParameterizedTypeReference<T> bodyType) {
|
||||
|
||||
this.invokedMethodName = methodName;
|
||||
this.requestValues = requestValues;
|
||||
this.bodyType = bodyType;
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user