From b6c0c5bb80d08b3e465add097c51860b029ab605 Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 12 Jul 2023 15:32:10 +0300 Subject: [PATCH 1/2] Properly populate metrics uri in presence of auth failures We now are able for RESTEasy Reactive to determine the URI template even when auth failures occur Fixes: #24938 --- .../deployment/ObservabilityProcessor.java | 32 ++++- .../observability/ObservabilityHandler.java | 8 +- .../ObservabilityIntegrationRecorder.java | 128 ++++++++++++++++++ .../observability/ObservabilityUtil.java | 15 ++ .../http/deployment/FilterBuildItem.java | 18 +++ .../micrometer-prometheus/pom.xml | 19 +++ .../prometheus/SecuredResource.java | 15 ++ .../src/main/resources/application.properties | 11 ++ .../PrometheusMetricsRegistryTest.java | 13 ++ 9 files changed, 251 insertions(+), 8 deletions(-) create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java create mode 100644 extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java create mode 100644 integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/SecuredResource.java diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java index 62214df491082..54a8626b5fbea 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/deployment/src/main/java/io/quarkus/resteasy/reactive/server/deployment/ObservabilityProcessor.java @@ -13,19 +13,21 @@ import io.quarkus.deployment.Capabilities; import io.quarkus.deployment.Capability; import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; import io.quarkus.deployment.metrics.MetricsCapabilityBuildItem; import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityCustomizer; +import io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityIntegrationRecorder; import io.quarkus.resteasy.reactive.server.spi.MethodScannerBuildItem; import io.quarkus.runtime.metrics.MetricsFactory; +import io.quarkus.vertx.http.deployment.FilterBuildItem; public class ObservabilityProcessor { @BuildStep - MethodScannerBuildItem integrateObservability(Capabilities capabilities, + MethodScannerBuildItem methodScanner(Capabilities capabilities, Optional metricsCapability) { - boolean integrationNeeded = (capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || - (metricsCapability.isPresent() - && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER))); + boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability); if (!integrationNeeded) { return null; } @@ -38,4 +40,26 @@ public List scan(MethodInfo method, ClassInfo actualEndp }); } + @BuildStep + @Record(value = ExecutionTime.STATIC_INIT) + FilterBuildItem preAuthFailureFilter(Capabilities capabilities, + Optional metricsCapability, + ObservabilityIntegrationRecorder recorder, + ResteasyReactiveDeploymentBuildItem deployment) { + boolean integrationNeeded = integrationNeeded(capabilities, metricsCapability); + if (!integrationNeeded) { + return null; + } + + return FilterBuildItem.ofPreAuthenticationFailureHandler( + recorder.preAuthFailureHandler(deployment.getDeployment())); + } + + private boolean integrationNeeded(Capabilities capabilities, + Optional metricsCapability) { + return capabilities.isPresent(Capability.OPENTELEMETRY_TRACER) || + (metricsCapability.isPresent() + && metricsCapability.get().metricsSupported(MetricsFactory.MICROMETER)); + } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java index 3e49617d012d8..a448289a89249 100644 --- a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityHandler.java @@ -1,11 +1,12 @@ package io.quarkus.resteasy.reactive.server.runtime.observability; +import static io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityUtil.*; + import java.util.regex.Pattern; import org.jboss.resteasy.reactive.server.core.ResteasyReactiveRequestContext; import org.jboss.resteasy.reactive.server.spi.ServerRestHandler; -import io.vertx.core.http.impl.HttpServerRequestInternal; import io.vertx.ext.web.RoutingContext; public class ObservabilityHandler implements ServerRestHandler { @@ -25,8 +26,7 @@ public void setTemplatePath(String templatePath) { @Override public void handle(ResteasyReactiveRequestContext requestContext) throws Exception { - - ((HttpServerRequestInternal) (requestContext.unwrap(RoutingContext.class).request())).context() - .putLocal("UrlPathTemplate", templatePath); + setUrlPathTemplate(requestContext.unwrap(RoutingContext.class), templatePath); } + } diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java new file mode 100644 index 0000000000000..72acb0831b1e6 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityIntegrationRecorder.java @@ -0,0 +1,128 @@ +package io.quarkus.resteasy.reactive.server.runtime.observability; + +import static io.quarkus.resteasy.reactive.server.runtime.observability.ObservabilityUtil.*; + +import jakarta.ws.rs.HttpMethod; + +import org.jboss.logging.Logger; +import org.jboss.resteasy.reactive.server.core.Deployment; +import org.jboss.resteasy.reactive.server.handlers.ClassRoutingHandler; +import org.jboss.resteasy.reactive.server.mapping.RequestMapper; + +import io.quarkus.runtime.RuntimeValue; +import io.quarkus.runtime.annotations.Recorder; +import io.quarkus.security.AuthenticationException; +import io.quarkus.security.ForbiddenException; +import io.quarkus.security.UnauthorizedException; +import io.vertx.core.Handler; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class ObservabilityIntegrationRecorder { + + private static final Logger log = Logger.getLogger(ObservabilityIntegrationRecorder.class); + + /** + * Returns a handler that sets the special property URI Template path needed by various observability integrations + */ + public Handler preAuthFailureHandler(RuntimeValue deploymentRV) { + return new Handler() { + @Override + public void handle(RoutingContext event) { + if (shouldHandle(event)) { + try { + setTemplatePath(event, deploymentRV.getValue()); + } catch (Exception e) { + log.debug("Unable to set template path for observability", e); + } + } + event.next(); + } + + private boolean shouldHandle(RoutingContext event) { + if (!event.failed()) { + return false; + } + return event.failure() instanceof AuthenticationException + || event.failure() instanceof ForbiddenException + || event.failure() instanceof UnauthorizedException; + } + + private void setTemplatePath(RoutingContext rc, Deployment deployment) { + // do what RestInitialHandler does + var initMappers = new RequestMapper<>(deployment.getClassMappers()); + var requestMatch = initMappers.map(getPathWithoutPrefix(rc, deployment)); + var remaining = requestMatch.remaining.isEmpty() ? "/" : requestMatch.remaining; + + var serverRestHandlers = requestMatch.value.handlers; + if (serverRestHandlers == null || serverRestHandlers.length < 1) { + // nothing we can do + return; + } + var firstHandler = serverRestHandlers[0]; + if (!(firstHandler instanceof ClassRoutingHandler)) { + // nothing we can do + return; + } + + var classRoutingHandler = (ClassRoutingHandler) firstHandler; + var mappers = classRoutingHandler.getMappers(); + + var requestMethod = rc.request().method().name(); + + // do what ClassRoutingHandler does + var mapper = mappers.get(requestMethod); + if (mapper == null) { + if (requestMethod.equals(HttpMethod.HEAD) || requestMethod.equals(HttpMethod.OPTIONS)) { + mapper = mappers.get(HttpMethod.GET); + } + if (mapper == null) { + mapper = mappers.get(null); + } + if (mapper == null) { + // can't match the path + return; + } + } + var target = mapper.map(remaining); + if (target == null) { + if (requestMethod.equals(HttpMethod.HEAD)) { + mapper = mappers.get(HttpMethod.GET); + if (mapper != null) { + target = mapper.map(remaining); + } + } + + if (target == null) { + // can't match the path + return; + } + } + + var templatePath = requestMatch.template.template + target.template.template; + if (templatePath.endsWith("/")) { + templatePath = templatePath.substring(0, templatePath.length() - 1); + } + + setUrlPathTemplate(rc, templatePath); + } + + public String getPath(RoutingContext rc) { + return rc.normalizedPath(); + } + + public String getPathWithoutPrefix(RoutingContext rc, Deployment deployment) { + String path = getPath(rc); + if (path != null) { + String prefix = deployment.getPrefix(); + if (!prefix.isEmpty()) { + if (path.startsWith(prefix)) { + return path.substring(prefix.length()); + } + } + } + return path; + } + }; + } +} diff --git a/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java new file mode 100644 index 0000000000000..2576e8a7de7d4 --- /dev/null +++ b/extensions/resteasy-reactive/quarkus-resteasy-reactive/runtime/src/main/java/io/quarkus/resteasy/reactive/server/runtime/observability/ObservabilityUtil.java @@ -0,0 +1,15 @@ +package io.quarkus.resteasy.reactive.server.runtime.observability; + +import io.vertx.core.http.impl.HttpServerRequestInternal; +import io.vertx.ext.web.RoutingContext; + +final class ObservabilityUtil { + + private ObservabilityUtil() { + } + + static void setUrlPathTemplate(RoutingContext routingContext, String templatePath) { + ((HttpServerRequestInternal) (routingContext.request())).context() + .putLocal("UrlPathTemplate", templatePath); + } +} diff --git a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java index ab7c6f4fd9883..b9c6c012347b2 100644 --- a/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java +++ b/extensions/vertx-http/deployment/src/main/java/io/quarkus/vertx/http/deployment/FilterBuildItem.java @@ -35,6 +35,15 @@ public FilterBuildItem(Handler handler, int priority) { this.isFailureHandler = false; } + private FilterBuildItem(Handler handler, int priority, boolean checkPriority, boolean isFailureHandler) { + this.handler = handler; + if (checkPriority) { + checkPriority(priority); + } + this.priority = priority; + this.isFailureHandler = isFailureHandler; + } + /** * Creates a new instance of {@link FilterBuildItem} with an authentication failure handler. * @@ -54,6 +63,15 @@ public static FilterBuildItem ofAuthenticationFailureHandler(Handler authFailureHandler) { + return new FilterBuildItem(authFailureHandler, AUTH_FAILURE_HANDLER + 1, false, true); + } + private void checkPriority(int priority) { if (priority < 0) { throw new IllegalArgumentException("`priority` must be positive"); diff --git a/integration-tests/micrometer-prometheus/pom.xml b/integration-tests/micrometer-prometheus/pom.xml index 1dcda8fc1333f..7c91682d35eda 100644 --- a/integration-tests/micrometer-prometheus/pom.xml +++ b/integration-tests/micrometer-prometheus/pom.xml @@ -51,6 +51,12 @@ quarkus-opentelemetry + + + io.quarkus + quarkus-elytron-security-properties-file + + io.quarkus @@ -163,6 +169,19 @@ + + io.quarkus + quarkus-elytron-security-properties-file-deployment + ${project.version} + pom + test + + + * + * + + + org.awaitility awaitility diff --git a/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/SecuredResource.java b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/SecuredResource.java new file mode 100644 index 0000000000000..6702678e20d88 --- /dev/null +++ b/integration-tests/micrometer-prometheus/src/main/java/io/quarkus/it/micrometer/prometheus/SecuredResource.java @@ -0,0 +1,15 @@ +package io.quarkus.it.micrometer.prometheus; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.PathParam; + +@Path("/secured") +public class SecuredResource { + + @GET + @Path("item/{id}") + public String item(@PathParam("id") String id) { + return "return message with id " + id; + } +} diff --git a/integration-tests/micrometer-prometheus/src/main/resources/application.properties b/integration-tests/micrometer-prometheus/src/main/resources/application.properties index 855e6fcbbe7c3..baa8404536654 100644 --- a/integration-tests/micrometer-prometheus/src/main/resources/application.properties +++ b/integration-tests/micrometer-prometheus/src/main/resources/application.properties @@ -28,3 +28,14 @@ deployment.env=test # Disable Kubernetes dev services as not supported on Windows quarkus.kubernetes-client.devservices.enabled=false + + +quarkus.security.users.embedded.enabled=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.scott=reader +quarkus.security.users.embedded.users.stuart=writer +quarkus.security.users.embedded.roles.scott=READER +quarkus.security.users.embedded.roles.stuart=READER,WRITER +quarkus.http.auth.permission.secured.policy=authenticated +quarkus.http.auth.permission.secured.paths=/secured/* + diff --git a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java index 696c959f92026..67c1c7cf63d31 100644 --- a/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java +++ b/integration-tests/micrometer-prometheus/src/test/java/io/quarkus/it/micrometer/prometheus/PrometheusMetricsRegistryTest.java @@ -89,6 +89,15 @@ void testTemplatedPathOnClass() { @Test @Order(10) + void testSecuredEndpoint() { + when().get("/secured/item/123").then().statusCode(401); + given().auth().preemptive().basic("foo", "bar").when().get("/secured/item/321").then().statusCode(401); + given().auth().preemptive().basic("scott", "reader").when().get("/secured/item/123").then().statusCode(200); + given().auth().preemptive().basic("stuart", "writer").when().get("/secured/item/321").then().statusCode(200); + } + + @Test + @Order(11) void testPrometheusScrapeEndpointTextPlain() { RestAssured.given().header("Accept", TextFormat.CONTENT_TYPE_004) .when().get("/q/metrics") @@ -111,6 +120,10 @@ void testPrometheusScrapeEndpointTextPlain() { .body(containsString("status=\"200\"")) .body(containsString("uri=\"/message\"")) .body(containsString("uri=\"/message/item/{id}\"")) + .body(containsString("status=\"200\",uri=\"/message/item/{id}\"")) + .body(containsString("uri=\"/secured/item/{id}\"")) + .body(containsString("status=\"200\",uri=\"/secured/item/{id}\"")) + .body(containsString("status=\"401\",uri=\"/secured/item/{id}\"")) .body(containsString("outcome=\"SUCCESS\"")) .body(containsString("dummy=\"value\"")) .body(containsString("foo=\"bar\"")) From 51385e6813f38eef66dd069a8e4003f601fff77f Mon Sep 17 00:00:00 2001 From: Georgios Andrianakis Date: Wed, 12 Jul 2023 18:58:31 +0300 Subject: [PATCH 2/2] Add test to OpenTelemetry for URI template verification --- .../opentelemetry-reactive/pom.xml | 19 +++++++++ .../reactive/SecuredResource.java | 15 +++++++ .../src/main/resources/application.properties | 9 +++++ .../reactive/OpenTelemetryReactiveTest.java | 39 +++++++++++++++---- 4 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/SecuredResource.java diff --git a/integration-tests/opentelemetry-reactive/pom.xml b/integration-tests/opentelemetry-reactive/pom.xml index c213fc02432ba..460516176fcd3 100644 --- a/integration-tests/opentelemetry-reactive/pom.xml +++ b/integration-tests/opentelemetry-reactive/pom.xml @@ -41,6 +41,12 @@ opentelemetry-sdk-testing + + + io.quarkus + quarkus-elytron-security-properties-file + + io.quarkus @@ -121,6 +127,19 @@ + + io.quarkus + quarkus-elytron-security-properties-file-deployment + ${project.version} + pom + test + + + * + * + + + diff --git a/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/SecuredResource.java b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/SecuredResource.java new file mode 100644 index 0000000000000..126184518bf61 --- /dev/null +++ b/integration-tests/opentelemetry-reactive/src/main/java/io/quarkus/it/opentelemetry/reactive/SecuredResource.java @@ -0,0 +1,15 @@ +package io.quarkus.it.opentelemetry.reactive; + +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; + +import org.jboss.resteasy.reactive.RestPath; + +@Path("secured") +public class SecuredResource { + @GET + @Path("item/{value}") + public String get(@RestPath String value) { + return "Received: " + value; + } +} diff --git a/integration-tests/opentelemetry-reactive/src/main/resources/application.properties b/integration-tests/opentelemetry-reactive/src/main/resources/application.properties index 7485ece03853e..4b779b476294f 100644 --- a/integration-tests/opentelemetry-reactive/src/main/resources/application.properties +++ b/integration-tests/opentelemetry-reactive/src/main/resources/application.properties @@ -2,3 +2,12 @@ quarkus.rest-client.client.url=${test.url quarkus.otel.bsp.schedule.delay=100 quarkus.otel.bsp.export.timeout=5s + +quarkus.security.users.embedded.enabled=true +quarkus.security.users.embedded.plain-text=true +quarkus.security.users.embedded.users.scott=reader +quarkus.security.users.embedded.users.stuart=writer +quarkus.security.users.embedded.roles.scott=READER +quarkus.security.users.embedded.roles.stuart=READER,WRITER +quarkus.http.auth.permission.secured.policy=authenticated +quarkus.http.auth.permission.secured.paths=/secured/* diff --git a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java index 26bf417e99ca0..5b9ef8c7f9818 100644 --- a/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java +++ b/integration-tests/opentelemetry-reactive/src/test/java/io/quarkus/it/opentelemetry/reactive/OpenTelemetryReactiveTest.java @@ -10,7 +10,9 @@ import static io.quarkus.it.opentelemetry.reactive.Utils.getSpans; import static io.quarkus.it.opentelemetry.reactive.Utils.getSpansByKindAndParentId; import static io.restassured.RestAssured.given; +import static io.restassured.RestAssured.when; import static java.net.HttpURLConnection.HTTP_OK; +import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.stream.Collectors.toSet; import static org.assertj.core.api.Assertions.assertThat; import static org.awaitility.Awaitility.await; @@ -21,7 +23,6 @@ import java.util.List; import java.util.Map; import java.util.Optional; -import java.util.concurrent.TimeUnit; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; @@ -36,7 +37,7 @@ public class OpenTelemetryReactiveTest { @AfterEach void reset() { given().get("/reset").then().statusCode(HTTP_OK); - await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 0); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 0); } @Test @@ -49,7 +50,7 @@ void get() { .statusCode(200) .body(equalTo("Hello Naruto")); - await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 2); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 2); List> spans = getSpans(); assertEquals(2, spans.size()); assertEquals(spans.get(0).get("traceId"), spans.get(1).get("traceId")); @@ -78,7 +79,7 @@ void reactiveException() { } private static void assertExceptionRecorded() { - await().atMost(5, TimeUnit.SECONDS).until(() -> getExceptionEventData().size() == 1); + await().atMost(5, SECONDS).until(() -> getExceptionEventData().size() == 1); assertThat(getExceptionEventData()).singleElement().satisfies(s -> { assertThat(s).contains("dummy"); }); @@ -94,7 +95,7 @@ void post() { .statusCode(200) .body(equalTo("Hello Naruto")); - await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 2); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 2); List> spans = getSpans(); assertEquals(2, spans.size()); assertEquals(spans.get(0).get("traceId"), spans.get(1).get("traceId")); @@ -109,7 +110,7 @@ void multipleUsingChain() { .statusCode(200) .body(equalTo("Hello Naruto and Hello Goku")); - await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 7); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 7); List> spans = getSpans(); assertEquals(7, spans.size()); @@ -158,7 +159,7 @@ void multipleUsingCombine() { .statusCode(200) .body(equalTo("Hello Naruto and Hello Goku")); - await().atMost(5, TimeUnit.SECONDS).until(() -> getSpans().size() == 7); + await().atMost(5, SECONDS).until(() -> getSpans().size() == 7); List> spans = getSpans(); assertEquals(7, spans.size()); @@ -197,4 +198,28 @@ void multipleUsingCombine() { Map gokuInternal = getSpanByKindAndParentId(spans, INTERNAL, gokuServer.get("spanId")); assertEquals("helloGet", gokuInternal.get("name")); } + + @Test + public void securedInvalidCredential() { + given().auth().preemptive().basic("scott", "reader2").when().get("/secured/item/something") + .then() + .statusCode(401); + + await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); + assertThat(getSpans()).singleElement().satisfies(m -> { + assertThat(m).extractingByKey("name").isEqualTo("GET /secured/item/{value}"); + }); + } + + @Test + public void securedProperCredentials() { + given().auth().preemptive().basic("scott", "reader").when().get("/secured/item/something") + .then() + .statusCode(200); + + await().atMost(5, SECONDS).until(() -> getSpans().size() == 1); + assertThat(getSpans()).singleElement().satisfies(m -> { + assertThat(m).extractingByKey("name").isEqualTo("GET /secured/item/{value}"); + }); + } }