544 lines
21 KiB
Groovy
544 lines
21 KiB
Groovy
/*
|
|
* Copyright The OpenTelemetry Authors
|
|
* SPDX-License-Identifier: Apache-2.0
|
|
*/
|
|
|
|
import static io.opentelemetry.api.trace.SpanKind.INTERNAL
|
|
import static io.opentelemetry.api.trace.SpanKind.SERVER
|
|
import static io.opentelemetry.api.trace.StatusCode.ERROR
|
|
|
|
import io.opentelemetry.instrumentation.test.AgentInstrumentationSpecification
|
|
import io.opentelemetry.semconv.trace.attributes.SemanticAttributes
|
|
import io.opentelemetry.testing.internal.armeria.client.ClientRequestContext
|
|
import io.opentelemetry.testing.internal.armeria.client.DecoratingHttpClientFunction
|
|
import io.opentelemetry.testing.internal.armeria.client.HttpClient
|
|
import io.opentelemetry.testing.internal.armeria.client.WebClient
|
|
import io.opentelemetry.testing.internal.armeria.common.HttpHeaderNames
|
|
import io.opentelemetry.testing.internal.armeria.common.HttpRequest
|
|
import io.opentelemetry.testing.internal.armeria.common.HttpResponse
|
|
import org.springframework.boot.test.context.SpringBootTest
|
|
import org.springframework.boot.test.context.TestConfiguration
|
|
import org.springframework.boot.web.embedded.netty.NettyReactiveWebServerFactory
|
|
import org.springframework.boot.web.server.LocalServerPort
|
|
import org.springframework.context.annotation.Bean
|
|
import org.springframework.web.server.ResponseStatusException
|
|
import server.EchoHandlerFunction
|
|
import server.FooModel
|
|
import server.SpringWebFluxTestApplication
|
|
import server.TestController
|
|
import spock.lang.Unroll
|
|
import util.SpringWebfluxTestUtil
|
|
|
|
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = [SpringWebFluxTestApplication, ForceNettyAutoConfiguration])
|
|
class SpringWebfluxTest extends AgentInstrumentationSpecification {
|
|
@TestConfiguration
|
|
static class ForceNettyAutoConfiguration {
|
|
@Bean
|
|
NettyReactiveWebServerFactory nettyFactory() {
|
|
return new NettyReactiveWebServerFactory()
|
|
}
|
|
}
|
|
|
|
static final String INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX = SpringWebFluxTestApplication.getName() + "\$"
|
|
static final String SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX = SpringWebFluxTestApplication.getSimpleName() + "\$"
|
|
|
|
@LocalServerPort
|
|
int port
|
|
|
|
WebClient client
|
|
|
|
def setup() {
|
|
client = WebClient.builder("h1c://localhost:$port")
|
|
.decorator(new DecoratingHttpClientFunction() {
|
|
// https://github.com/line/armeria/issues/2489
|
|
@Override
|
|
HttpResponse execute(HttpClient delegate, ClientRequestContext ctx, HttpRequest req) throws Exception {
|
|
return HttpResponse.from(delegate.execute(ctx, req).aggregate().thenApply {resp ->
|
|
if (resp.status().isRedirection()) {
|
|
return delegate.execute(ctx, HttpRequest.of(req.method(), resp.headers().get(HttpHeaderNames.LOCATION)))
|
|
}
|
|
return resp.toHttpResponse()
|
|
})
|
|
}
|
|
})
|
|
.build()
|
|
}
|
|
|
|
def cleanup() {
|
|
SpringWebfluxTestUtil.waitForRequestsToComplete()
|
|
}
|
|
|
|
@Unroll
|
|
def "Basic GET test #testName"() {
|
|
when:
|
|
def response = client.get(urlPath).aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 200
|
|
response.contentUtf8() == expectedResponseBody
|
|
assertTraces(1) {
|
|
trace(0, 2) {
|
|
span(0) {
|
|
name urlPathWithVariables
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle")
|
|
} else {
|
|
// Annotation API
|
|
name TestController.getSimpleName() + "." + annotatedMethod
|
|
}
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
}
|
|
} else {
|
|
// Annotation API
|
|
"spring-webflux.handler.type" TestController.getName()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
where:
|
|
testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody
|
|
"functional API without parameters" | "/greet" | "/greet" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE
|
|
"functional API with one parameter" | "/greet/WORLD" | "/greet/{name}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " WORLD"
|
|
"functional API with two parameters" | "/greet/World/Test1" | "/greet/{name}/{word}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " World Test1"
|
|
"functional API delayed response" | "/greet-delayed" | "/greet-delayed" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE
|
|
|
|
"annotation API without parameters" | "/foo" | "/foo" | "getFooModel" | new FooModel(0L, "DEFAULT").toString()
|
|
"annotation API with one parameter" | "/foo/1" | "/foo/{id}" | "getFooModel" | new FooModel(1L, "pass").toString()
|
|
"annotation API with two parameters" | "/foo/2/world" | "/foo/{id}/{name}" | "getFooModel" | new FooModel(2L, "world").toString()
|
|
"annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString()
|
|
}
|
|
|
|
@Unroll
|
|
def "GET test with async response #testName"() {
|
|
when:
|
|
def response = client.get(urlPath).aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 200
|
|
response.contentUtf8() == expectedResponseBody
|
|
assertTraces(1) {
|
|
trace(0, 3) {
|
|
span(0) {
|
|
name urlPathWithVariables
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle")
|
|
} else {
|
|
// Annotation API
|
|
name TestController.getSimpleName() + "." + annotatedMethod
|
|
}
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
}
|
|
} else {
|
|
// Annotation API
|
|
"spring-webflux.handler.type" TestController.getName()
|
|
}
|
|
}
|
|
}
|
|
span(2) {
|
|
name "tracedMethod"
|
|
childOf span(0)
|
|
attributes {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
where:
|
|
testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody
|
|
"functional API traced method from mono" | "/greet-mono-from-callable/4" | "/greet-mono-from-callable/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 4"
|
|
"functional API traced method with delay" | "/greet-delayed-mono/6" | "/greet-delayed-mono/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 6"
|
|
|
|
"annotation API traced method from mono" | "/foo-mono-from-callable/7" | "/foo-mono-from-callable/{id}" | "getMonoFromCallable" | new FooModel(7L, "tracedMethod").toString()
|
|
"annotation API traced method with delay" | "/foo-delayed-mono/9" | "/foo-delayed-mono/{id}" | "getFooDelayedMono" | new FooModel(9L, "tracedMethod").toString()
|
|
}
|
|
|
|
/*
|
|
This test differs from the previous in one important aspect.
|
|
The test above calls endpoints which does not create any spans during their invocation.
|
|
They merely assemble reactive pipeline where some steps create spans.
|
|
Thus all those spans are created when WebFlux span created by DispatcherHandlerInstrumentation
|
|
has already finished. Therefore, they have `SERVER` span as their parent.
|
|
|
|
This test below calls endpoints which do create spans right inside endpoint handler.
|
|
Therefore, in theory, those spans should have INTERNAL span created by DispatcherHandlerInstrumentation
|
|
as their parent. But there is a difference how Spring WebFlux handles functional endpoints
|
|
(created in server.SpringWebFluxTestApplication.greetRouterFunction) and annotated endpoints
|
|
(created in server.TestController).
|
|
In the former case org.springframework.web.reactive.function.server.support.HandlerFunctionAdapter.handle
|
|
calls handler function directly. Thus "tracedMethod" span below has INTERNAL handler span as its parent.
|
|
In the latter case org.springframework.web.reactive.result.method.annotation.RequestMappingHandlerAdapter.handle
|
|
merely wraps handler call into Mono and thus actual invocation of handler function happens later,
|
|
when INTERNAL handler span has already finished. Thus, "tracedMethod" has SERVER Netty span as its parent.
|
|
*/
|
|
def "Create span during handler function"() {
|
|
when:
|
|
def response = client.get(urlPath).aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 200
|
|
response.contentUtf8() == expectedResponseBody
|
|
assertTraces(1) {
|
|
trace(0, 3) {
|
|
span(0) {
|
|
name urlPathWithVariables
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle")
|
|
} else {
|
|
// Annotation API
|
|
name TestController.getSimpleName() + "." + annotatedMethod
|
|
}
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
}
|
|
} else {
|
|
// Annotation API
|
|
"spring-webflux.handler.type" TestController.getName()
|
|
}
|
|
}
|
|
}
|
|
span(2) {
|
|
name "tracedMethod"
|
|
childOf span(annotatedMethod ? 0 : 1)
|
|
attributes {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
where:
|
|
testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody
|
|
"functional API traced method" | "/greet-traced-method/5" | "/greet-traced-method/{id}" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE + " 5"
|
|
"annotation API traced method" | "/foo-traced-method/8" | "/foo-traced-method/{id}" | "getTracedMethod" | new FooModel(8L, "tracedMethod").toString()
|
|
}
|
|
|
|
def "404 GET test"() {
|
|
when:
|
|
def response = client.get("/notfoundgreet").aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 404
|
|
assertTraces(1) {
|
|
trace(0, 2) {
|
|
span(0) {
|
|
name "/**"
|
|
kind SERVER
|
|
hasNoParent()
|
|
status ERROR
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/notfoundgreet"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 404
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
name "ResourceWebHandler.handle"
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
status ERROR
|
|
errorEvent(ResponseStatusException, String)
|
|
attributes {
|
|
"spring-webflux.handler.type" "org.springframework.web.reactive.resource.ResourceWebHandler"
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
def "Basic POST test"() {
|
|
setup:
|
|
String echoString = "TEST"
|
|
when:
|
|
def response = client.post("/echo", echoString).aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 202
|
|
response.contentUtf8() == echoString
|
|
assertTraces(1) {
|
|
trace(0, 3) {
|
|
span(0) {
|
|
name "/echo"
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/echo"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "POST"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 202
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
name EchoHandlerFunction.getSimpleName() + ".handle"
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(EchoHandlerFunction.getName())
|
|
}
|
|
}
|
|
}
|
|
span(2) {
|
|
name "echo"
|
|
childOf span(1)
|
|
attributes {
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Unroll
|
|
def "GET to bad endpoint #testName"() {
|
|
when:
|
|
def response = client.get(urlPath).aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 500
|
|
assertTraces(1) {
|
|
trace(0, 2) {
|
|
span(0) {
|
|
name urlPathWithVariables
|
|
kind SERVER
|
|
status ERROR
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port$urlPath"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 500
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle")
|
|
} else {
|
|
// Annotation API
|
|
name TestController.getSimpleName() + "." + annotatedMethod
|
|
}
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
status ERROR
|
|
errorEvent(IllegalStateException, "bad things happen")
|
|
attributes {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
}
|
|
} else {
|
|
// Annotation API
|
|
"spring-webflux.handler.type" TestController.getName()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
where:
|
|
testName | urlPath | urlPathWithVariables | annotatedMethod
|
|
"functional API fail fast" | "/greet-failfast/1" | "/greet-failfast/{id}" | null
|
|
"functional API fail Mono" | "/greet-failmono/1" | "/greet-failmono/{id}" | null
|
|
|
|
"annotation API fail fast" | "/foo-failfast/1" | "/foo-failfast/{id}" | "getFooFailFast"
|
|
"annotation API fail Mono" | "/foo-failmono/1" | "/foo-failmono/{id}" | "getFooFailMono"
|
|
}
|
|
|
|
def "Redirect test"() {
|
|
setup:
|
|
String finalUrl = "http://localhost:$port/double-greet"
|
|
|
|
when:
|
|
def response = client.get("/double-greet-redirect").aggregate().join()
|
|
|
|
then:
|
|
response.status().code() == 200
|
|
assertTraces(2) {
|
|
// TODO: why order of spans is different in these traces?
|
|
trace(0, 2) {
|
|
span(0) {
|
|
name "/double-greet-redirect"
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" "http://localhost:$port/double-greet-redirect"
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 307
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
name "RedirectComponent.lambda"
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return (tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
|| tagVal.contains("Lambda"))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
trace(1, 2) {
|
|
span(0) {
|
|
name "/double-greet"
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" finalUrl
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
nameContains(SpringWebFluxTestApplication.getSimpleName() + "\$", ".handle")
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
@Unroll
|
|
def "Multiple GETs to delaying route #testName"() {
|
|
setup:
|
|
def requestsCount = 50 // Should be more than 2x CPUs to fish out some bugs
|
|
def url = "http://localhost:$port$urlPath"
|
|
when:
|
|
def responses = (0..requestsCount - 1).collect { client.get(urlPath).aggregate().join() }
|
|
|
|
then:
|
|
responses.every { it.status().code() == 200 }
|
|
responses.every { it.contentUtf8() == expectedResponseBody }
|
|
assertTraces(responses.size()) {
|
|
responses.eachWithIndex { def response, int i ->
|
|
trace(i, 2) {
|
|
span(0) {
|
|
name urlPathWithVariables
|
|
kind SERVER
|
|
hasNoParent()
|
|
attributes {
|
|
"${SemanticAttributes.NET_PEER_IP.key}" "127.0.0.1"
|
|
"${SemanticAttributes.NET_PEER_PORT.key}" Long
|
|
"${SemanticAttributes.HTTP_URL.key}" url
|
|
"${SemanticAttributes.HTTP_METHOD.key}" "GET"
|
|
"${SemanticAttributes.HTTP_STATUS_CODE.key}" 200
|
|
"${SemanticAttributes.HTTP_FLAVOR.key}" "1.1"
|
|
"${SemanticAttributes.HTTP_USER_AGENT.key}" String
|
|
}
|
|
}
|
|
span(1) {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
nameContains(SPRING_APP_CLASS_ANON_NESTED_CLASS_PREFIX, ".handle")
|
|
} else {
|
|
// Annotation API
|
|
name TestController.getSimpleName() + "." + annotatedMethod
|
|
}
|
|
kind INTERNAL
|
|
childOf span(0)
|
|
attributes {
|
|
if (annotatedMethod == null) {
|
|
// Functional API
|
|
"spring-webflux.handler.type" { String tagVal ->
|
|
return tagVal.contains(INNER_HANDLER_FUNCTION_CLASS_TAG_PREFIX)
|
|
}
|
|
} else {
|
|
// Annotation API
|
|
"spring-webflux.handler.type" TestController.getName()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
where:
|
|
testName | urlPath | urlPathWithVariables | annotatedMethod | expectedResponseBody
|
|
"functional API delayed response" | "/greet-delayed" | "/greet-delayed" | null | SpringWebFluxTestApplication.GreetingHandler.DEFAULT_RESPONSE
|
|
"annotation API delayed response" | "/foo-delayed" | "/foo-delayed" | "getFooDelayed" | new FooModel(3L, "delayed").toString()
|
|
}
|
|
}
|