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,218 @@
|
||||
/*
|
||||
* 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 okhttp3.mockwebserver.MockResponse
|
||||
import okhttp3.mockwebserver.MockWebServer
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
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.*
|
||||
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 java.net.URI
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Kotlin integration tests for {@link HttpServiceProxyFactory HTTP Service proxy} using
|
||||
* {@link RestTemplate} and {@link MockWebServer}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class KotlinRestTemplateHttpServiceProxyTests {
|
||||
|
||||
private lateinit var server: MockWebServer
|
||||
|
||||
private lateinit var testService: TestService
|
||||
|
||||
@BeforeEach
|
||||
fun setUp() {
|
||||
server = MockWebServer()
|
||||
prepareResponse()
|
||||
testService = initTestService()
|
||||
}
|
||||
|
||||
private fun initTestService(): TestService {
|
||||
val restTemplate = RestTemplate()
|
||||
restTemplate.uriTemplateHandler = DefaultUriBuilderFactory(server.url("/").toString())
|
||||
return HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(RestTemplateAdapter.forTemplate(restTemplate))
|
||||
.build()
|
||||
.createClient(TestService::class.java)
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
fun shutDown() {
|
||||
server.shutdown()
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(InterruptedException::class)
|
||||
fun getRequest() {
|
||||
val response = testService.request
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(response).isEqualTo("Hello Spring!")
|
||||
assertThat(request.method).isEqualTo("GET")
|
||||
assertThat(request.path).isEqualTo("/test")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(InterruptedException::class)
|
||||
fun getRequestWithPathVariable() {
|
||||
val response = testService.getRequestWithPathVariable("456")
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(response.statusCode).isEqualTo(HttpStatus.OK)
|
||||
assertThat(response.body).isEqualTo("Hello Spring!")
|
||||
assertThat(request.method).isEqualTo("GET")
|
||||
assertThat(request.path).isEqualTo("/test/456")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(InterruptedException::class)
|
||||
fun getRequestWithDynamicUri() {
|
||||
val dynamicUri = server.url("/greeting/123").uri()
|
||||
|
||||
val response = testService.getRequestWithDynamicUri(dynamicUri, "456")
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(response.orElse("empty")).isEqualTo("Hello Spring!")
|
||||
assertThat(request.method).isEqualTo("GET")
|
||||
assertThat(request.requestUrl.uri()).isEqualTo(dynamicUri)
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(InterruptedException::class)
|
||||
fun postWithRequestHeader() {
|
||||
testService.postRequestWithHeader("testHeader", "testBody")
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(request.method).isEqualTo("POST")
|
||||
assertThat(request.path).isEqualTo("/test")
|
||||
assertThat(request.headers["testHeaderName"]).isEqualTo("testHeader")
|
||||
assertThat(request.body.readUtf8()).isEqualTo("testBody")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(Exception::class)
|
||||
fun formData() {
|
||||
val map: MultiValueMap<String, String> = LinkedMultiValueMap()
|
||||
map.add("param1", "value 1")
|
||||
map.add("param2", "value 2")
|
||||
|
||||
testService.postForm(map)
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(request.headers["Content-Type"])
|
||||
.isEqualTo("application/x-www-form-urlencoded;charset=UTF-8")
|
||||
assertThat(request.body.readUtf8()).isEqualTo("param1=value+1¶m2=value+2")
|
||||
}
|
||||
|
||||
// gh-30342
|
||||
@Test
|
||||
@Throws(InterruptedException::class)
|
||||
fun multipart() {
|
||||
val fileName = "testFileName"
|
||||
val originalFileName = "originalTestFileName"
|
||||
val file: MultipartFile = MockMultipartFile(fileName, originalFileName, MediaType.APPLICATION_JSON_VALUE,
|
||||
"test".toByteArray())
|
||||
|
||||
testService.postMultipart(file, "test2")
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(request.headers["Content-Type"]).startsWith("multipart/form-data;boundary=")
|
||||
assertThat(request.body.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
|
||||
@Throws(InterruptedException::class)
|
||||
fun putRequestWithCookies() {
|
||||
testService.putRequestWithCookies("test1", "test2")
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(request.method).isEqualTo("PUT")
|
||||
assertThat(request.getHeader("Cookie"))
|
||||
.isEqualTo("firstCookie=test1; secondCookie=test2")
|
||||
}
|
||||
|
||||
@Test
|
||||
@Throws(InterruptedException::class)
|
||||
fun putRequestWithSameNameCookies() {
|
||||
testService.putRequestWithSameNameCookies("test1", "test2")
|
||||
|
||||
val request = server.takeRequest()
|
||||
assertThat(request.method).isEqualTo("PUT")
|
||||
assertThat(request.getHeader("Cookie"))
|
||||
.isEqualTo("testCookie=test1; testCookie=test2")
|
||||
}
|
||||
|
||||
private fun prepareResponse() {
|
||||
val response = MockResponse()
|
||||
response.setHeader("Content-Type", "text/plain").setBody("Hello Spring!")
|
||||
server.enqueue(response)
|
||||
}
|
||||
|
||||
|
||||
private interface TestService {
|
||||
|
||||
@get:GetExchange("/test")
|
||||
val request: String
|
||||
|
||||
@GetExchange("/test/{id}")
|
||||
fun getRequestWithPathVariable(@PathVariable id: String): ResponseEntity<String>
|
||||
|
||||
@GetExchange("/test/{id}")
|
||||
fun getRequestWithDynamicUri(@Nullable uri: URI, @PathVariable id: String): Optional<String>
|
||||
|
||||
@PostExchange("/test")
|
||||
fun postRequestWithHeader(@RequestHeader("testHeaderName") testHeader: String,
|
||||
@RequestBody requestBody: String)
|
||||
|
||||
@PostExchange(contentType = "application/x-www-form-urlencoded")
|
||||
fun postForm(@RequestParam params: MultiValueMap<String, String>)
|
||||
|
||||
@PostExchange
|
||||
fun postMultipart(file: MultipartFile, @RequestPart anotherPart: String)
|
||||
|
||||
@PutExchange
|
||||
fun putRequestWithCookies(@CookieValue firstCookie: String,
|
||||
@CookieValue secondCookie: String)
|
||||
|
||||
@PutExchange
|
||||
fun putRequestWithSameNameCookies(@CookieValue("testCookie") firstCookie: String,
|
||||
@CookieValue("testCookie") secondCookie: String)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -20,6 +20,7 @@ import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.toList
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import org.assertj.core.api.Assertions.assertThat
|
||||
import org.assertj.core.api.Assertions.assertThatIllegalStateException
|
||||
import org.junit.jupiter.api.Test
|
||||
import org.springframework.core.ParameterizedTypeReference
|
||||
import org.springframework.http.HttpStatus
|
||||
@@ -30,64 +31,124 @@ import org.springframework.web.service.annotation.GetExchange
|
||||
* Kotlin tests for [HttpServiceMethod].
|
||||
*
|
||||
* @author Sebastien Deleuze
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class HttpServiceMethodKotlinTests {
|
||||
@Suppress("DEPRECATION")
|
||||
class KotlinHttpServiceMethodTests {
|
||||
|
||||
private val client = TestHttpClientAdapter()
|
||||
private val proxyFactory = HttpServiceProxyFactory.builder(client).build()
|
||||
private val webClientAdapter = TestHttpClientAdapter()
|
||||
private val httpExchangeAdapter = TestHttpExchangeAdapter()
|
||||
private val proxyFactory = HttpServiceProxyFactory.builder(webClientAdapter).build()
|
||||
private val blockingProxyFactory = HttpServiceProxyFactory.builder()
|
||||
.exchangeAdapter(httpExchangeAdapter).build()
|
||||
|
||||
@Test
|
||||
fun coroutinesService(): Unit = runBlocking {
|
||||
val service = proxyFactory.createClient(CoroutinesService::class.java)
|
||||
val service = proxyFactory.createClient(FunctionsService::class.java)
|
||||
|
||||
val stringBody = service.stringBody()
|
||||
assertThat(stringBody).isEqualTo("requestToBody")
|
||||
verifyClientInvocation("requestToBody", object : ParameterizedTypeReference<String>() {})
|
||||
assertThat(stringBody).isEqualTo("body")
|
||||
verifyClientInvocation("body", object : ParameterizedTypeReference<String>() {})
|
||||
|
||||
service.listBody()
|
||||
verifyClientInvocation("requestToBody", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||
verifyClientInvocation("body", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||
|
||||
val flowBody = service.flowBody()
|
||||
assertThat(flowBody.toList()).containsExactly("request", "To", "Body", "Flux")
|
||||
verifyClientInvocation("requestToBodyFlux", object : ParameterizedTypeReference<String>() {})
|
||||
verifyClientInvocation("bodyFlux", object : ParameterizedTypeReference<String>() {})
|
||||
|
||||
val stringEntity = service.stringEntity()
|
||||
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("requestToEntity"))
|
||||
verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference<String>() {})
|
||||
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("entity"))
|
||||
verifyClientInvocation("entity", object : ParameterizedTypeReference<String>() {})
|
||||
|
||||
service.listEntity()
|
||||
verifyClientInvocation("requestToEntity", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||
verifyClientInvocation("entity", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||
|
||||
val flowEntity = service.flowEntity()
|
||||
assertThat(flowEntity.statusCode).isEqualTo(HttpStatus.OK)
|
||||
assertThat(flowEntity.body!!.toList()).containsExactly("request", "To", "Entity", "Flux")
|
||||
verifyClientInvocation("requestToEntityFlux", object : ParameterizedTypeReference<String>() {})
|
||||
verifyClientInvocation("entityFlux", object : ParameterizedTypeReference<String>() {})
|
||||
}
|
||||
|
||||
private fun verifyClientInvocation(methodName: String, expectedBodyType: ParameterizedTypeReference<*>) {
|
||||
assertThat(client.invokedMethodName).isEqualTo(methodName)
|
||||
assertThat(client.bodyType).isEqualTo(expectedBodyType)
|
||||
@Test
|
||||
fun blockingServiceWithExchangeResponseFunction() {
|
||||
val service = blockingProxyFactory.createClient(BlockingFunctionsService::class.java)
|
||||
|
||||
val stringBody = service.stringBodyBlocking()
|
||||
assertThat(stringBody).isEqualTo("body")
|
||||
verifyTemplateInvocation("body", object : ParameterizedTypeReference<String>() {})
|
||||
|
||||
val listBody = service.listBodyBlocking()
|
||||
assertThat(listBody.size).isEqualTo(1)
|
||||
verifyTemplateInvocation("body", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||
|
||||
val stringEntity = service.stringEntityBlocking()
|
||||
assertThat(stringEntity).isEqualTo(ResponseEntity.ok<String>("entity"))
|
||||
verifyTemplateInvocation("entity", object : ParameterizedTypeReference<String>() {})
|
||||
|
||||
service.listEntityBlocking()
|
||||
verifyTemplateInvocation("entity", object : ParameterizedTypeReference<MutableList<String>>() {})
|
||||
}
|
||||
|
||||
@Test
|
||||
fun coroutineServiceWithExchangeResponseFunction() {
|
||||
assertThatIllegalStateException().isThrownBy {
|
||||
blockingProxyFactory.createClient(FunctionsService::class.java)
|
||||
}
|
||||
|
||||
assertThatIllegalStateException().isThrownBy {
|
||||
blockingProxyFactory.createClient(SuspendingFunctionsService::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
private fun verifyTemplateInvocation(methodReference: String, expectedBodyType: ParameterizedTypeReference<*>) {
|
||||
assertThat(httpExchangeAdapter.invokedMethodReference).isEqualTo(methodReference)
|
||||
assertThat(httpExchangeAdapter.bodyType).isEqualTo(expectedBodyType)
|
||||
}
|
||||
|
||||
private fun verifyClientInvocation(methodReference: String, expectedBodyType: ParameterizedTypeReference<*>) {
|
||||
assertThat(webClientAdapter.invokedMethodReference).isEqualTo(methodReference)
|
||||
assertThat(webClientAdapter.bodyType).isEqualTo(expectedBodyType)
|
||||
}
|
||||
|
||||
private interface CoroutinesService {
|
||||
|
||||
@GetExchange
|
||||
suspend fun stringBody(): String
|
||||
|
||||
@GetExchange
|
||||
suspend fun listBody(): MutableList<String>
|
||||
private interface FunctionsService : SuspendingFunctionsService {
|
||||
|
||||
@GetExchange
|
||||
fun flowBody(): Flow<String>
|
||||
|
||||
@GetExchange
|
||||
suspend fun stringEntity(): ResponseEntity<String>
|
||||
|
||||
@GetExchange
|
||||
suspend fun listEntity(): ResponseEntity<MutableList<String>>
|
||||
|
||||
@GetExchange
|
||||
fun flowEntity(): ResponseEntity<Flow<String>>
|
||||
}
|
||||
|
||||
private interface SuspendingFunctionsService : BlockingFunctionsService {
|
||||
|
||||
@GetExchange
|
||||
suspend fun stringBody(): String
|
||||
|
||||
@GetExchange
|
||||
suspend fun listBody(): MutableList<String>
|
||||
|
||||
@GetExchange
|
||||
suspend fun stringEntity(): ResponseEntity<String>
|
||||
|
||||
@GetExchange
|
||||
suspend fun listEntity(): ResponseEntity<MutableList<String>>
|
||||
}
|
||||
|
||||
private interface BlockingFunctionsService {
|
||||
|
||||
@GetExchange
|
||||
fun stringBodyBlocking(): String
|
||||
|
||||
@GetExchange
|
||||
fun listBodyBlocking(): MutableList<String>
|
||||
|
||||
@GetExchange
|
||||
fun stringEntityBlocking(): ResponseEntity<String>
|
||||
|
||||
@GetExchange
|
||||
fun listEntityBlocking(): ResponseEntity<MutableList<String>>
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user