Repository: helidon-io/helidon (java)

Issue:
Helidon 4.0.5 fails during HTTP/2 connection upgrade due to wrong Base64 decoding
## Environment Details
* Helidon Version: 4.0.5
* Helidon SE
* JDK version: Zulu 21.0.1
* OS: 6.1.71-1-MANJARO
* Docker version (if applicable): -

----------

I am trying to run a benchmark using Hyperfoil and Helidon 4.0.5 BUT Helidon returns 500 error during H2C connection upgrade (benchmark runs OK when using HTTP 1.1. HTTP2 server properly configured with dependency in pom.xml and tested with curl.


 Helidon shows this error message in the logs:

```
2024.02.20 09:09:28.091 Internal server error
io.helidon.http.RequestException: Internal error
	at io.helidon.http.RequestException$Builder.build(RequestException.java:139)
	at io.helidon.webserver.http1.Http1Connection.handle(Http1Connection.java:227)
	at io.helidon.webserver.ConnectionHandler.run(ConnectionHandler.java:165)
	at io.helidon.common.task.InterruptableTask.call(InterruptableTask.java:47)
	at io.helidon.webserver.ThreadPerTaskExecutor$ThreadBoundFuture.run(ThreadPerTaskExecutor.java:239)
	at java.base/java.lang.VirtualThread.run(VirtualThread.java:309)
Caused by: java.lang.IllegalArgumentException: Illegal base64 character 5f
	at java.base/java.util.Base64$Decoder.decode0(Base64.java:852)
	at java.base/java.util.Base64$Decoder.decode(Base64.java:570)
	at io.helidon.webserver.http2.Http2Upgrader.upgrade(Http2Upgrader.java:80)
	at io.helidon.webserver.http1.Http1Connection.handle(Http1Connection.java:177)
	... 4 more

```
I think the problem is that Helidon is NOT able to properly handle the HTTP2-Settings header. According to the RFC ( https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-http2-07#page-10) , HTTP"-Settings is encoded as a URL- and filename-safe Base64 encoding. That means that "_" is a valid char in this alphabet. But looks to me that Helidon is using plain Base64 that is wrong according to the specs.


![wireshark](https://github.com/helidon-io/helidon/assets/3510476/4ddb8018-4d0d-47f3-8ac9-924c9c73c7d1)

In this case, the HTTP2-Setting headers as produced by Hyperfoil is AAEAABAAAAIAAAABAAN______AAQAAP__AAUAAEAAAAYAACAA, that is valid one according to specs. I attach a picture with the proper detail of the Wireshark capture to illustrate the issue.

I think that Http2Upgrader should use 

`private static final Base64.Decoder BASE_64_DECODER = Base64.getUrlDecoder();`

instead of 

`private static final Base64.Decoder BASE_64_DECODER = Base64.getDecoder();`

I am using hyperfoil 0.24.1 with this config file to easily reproduce the issue:


```
name: helidonConstantRate
threads: 2
http:
  host: http://localhost:8080
  sharedConnections: 100
  allowHttp1x: false
  maxHttp2Streams: 100
ergonomics: # Disable stopping the scenario on 4xx or 5xx response
  autoRangeCheck: false
phases:
- helidon:
    constantRate:
      usersPerSec: 2000
      duration: 120s
      maxDuration: 180s
      scenario:
      - search:
        - httpRequest:
            GET: /simple-greet

```

UPDATE: I tested the recommended fix and it works as expected!!!!

Pull request description:
4.x: HTTP2-Settings needs to be encoded/decoded to Base64 with url dialect
Fixes #8399


Patch:
diff --git a/docs/pom.xml b/docs/pom.xml
index 5023ef83da..50387533dc 100644
--- a/docs/pom.xml
+++ b/docs/pom.xml
@@ -43,6 +43,10 @@
             <type>pom</type>
             <optional>true</optional>
         </dependency>
+        <dependency>
+            <groupId>org.mockito</groupId>
+            <artifactId>mockito-core</artifactId>
+        </dependency>
         <!-- snippets dependencies -->
         <dependency>
             <groupId>org.apache.activemq</groupId>
diff --git a/docs/src/main/asciidoc/sitegen.yaml b/docs/src/main/asciidoc/sitegen.yaml
index 865fd227e7..35f50b37b3 100644
--- a/docs/src/main/asciidoc/sitegen.yaml
+++ b/docs/src/main/asciidoc/sitegen.yaml
@@ -277,6 +277,7 @@ backend:
                   value: "analytics"
               - type: "MENU"
                 title: "Testing"
+                dir: "testing"
                 glyph:
                   type: "icon"
                   value: "thumbs_up_down"
diff --git a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java
index 2c7c9561b3..014c1283c3 100644
--- a/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java
+++ b/tracing/providers/opentelemetry/src/main/java/io/helidon/tracing/providers/opentelemetry/OpenTelemetryDataPropagationProvider.java
@@ -32,10 +32,15 @@ public class OpenTelemetryDataPropagationProvider
 
     @Override
     public OpenTelemetryContext data() {
-        return Contexts.context().map(context -> context.get(Span.class).map(span -> {
-            Tracer tracer = context.get(Tracer.class).orElseGet(OpenTelemetryTracerProvider::globalTracer);
-            return new OpenTelemetryContext(tracer, span);
-        }).orElse(null)).orElse(null);
+        // Use a tracer from the current context--because there is no notion in OTel of a "current" tracer--or from the
+        // global tracer.
+        Tracer tracer = Contexts.context().flatMap(ctx -> ctx.get(Tracer.class)).orElseGet(Tracer::global);
+
+        // Get the current span only from OTel's notion of the current span. We do not care what span might be set in the
+        // current context, because after that context was constructed user code could have closed the current span or set
+        // a new span as current.
+        Span span = OpenTelemetryTracerProvider.activeSpan().orElse(null);
+        return new OpenTelemetryContext(tracer, span);
     }
 
     @Override
@@ -51,7 +56,7 @@ public class OpenTelemetryDataPropagationProvider
 
     @Override
     public void propagateData(OpenTelemetryContext context) {
-        if (context != null) {
+        if (context != null && context.span != null) {
             context.scope = context.span.activate();
         }
     }
diff --git a/tracing/providers/zipkin/pom.xml b/tracing/providers/zipkin/pom.xml
index 2d0a706d4c..2369688362 100644
--- a/tracing/providers/zipkin/pom.xml
+++ b/tracing/providers/zipkin/pom.xml
@@ -115,6 +115,19 @@
                     </annotationProcessorPaths>
                 </configuration>
             </plugin>
+            <!--
+                Because the Helidon Zipkin provider does not itself implement the Helidon tracing API (it does so through
+                OpenTracing), exempt this build from a test that requires that.
+            -->
+            <plugin>
+                <groupId>org.apache.maven.plugins</groupId>
+                <artifactId>maven-surefire-plugin</artifactId>
+                <configuration>
+                    <excludes>
+                        <exclude>io.helidon.tracing.providers.tests.TestTracerAndSpanPropagation.java</exclude>
+                    </excludes>
+                </configuration>
+            </plugin>
         </plugins>
     </build>
 </project>
diff --git a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java
index 539b1a378f..abb5a8f25d 100644
--- a/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java
+++ b/webclient/http2/src/main/java/io/helidon/webclient/http2/Http2ClientConnectionHandler.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -208,7 +208,7 @@ class Http2ClientConnectionHandler {
                 .data();
         byte[] b = new byte[settingsFrameData.available()];
         settingsFrameData.read(b);
-        return Base64.getEncoder().encodeToString(b);
+        return Base64.getUrlEncoder().encodeToString(b);
     }
 
     private Http2ConnectionAttemptResult http1(Http2ClientImpl http2Client,
diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java
index 3713d68f4e..fa70fa15fb 100644
--- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java
+++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Connection.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -321,6 +321,11 @@ public class Http2Connection implements ServerConnection, InterruptableTask<Void
         return serverSettings;
     }
 
+    // jUnit Http2Settings pkg only visible test accessor.
+    Http2Settings clientSettings() {
+        return clientSettings;
+    }
+
     private void doHandle(Semaphore requestSemaphore) throws InterruptedException {
         myThread = Thread.currentThread();
         while (canRun && state != State.FINISHED) {
diff --git a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java
index 8ec43b0bc5..23209f273e 100644
--- a/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java
+++ b/webserver/http2/src/main/java/io/helidon/webserver/http2/Http2Upgrader.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2022, 2023 Oracle and/or its affiliates.
+ * Copyright (c) 2022, 2024 Oracle and/or its affiliates.
  *
  * Licensed under the Apache License, Version 2.0 (the "License");
  * you may not use this file except in compliance with the License.
@@ -43,7 +43,6 @@ public class Http2Upgrader implements Http1Upgrader {
                     + "Upgrade: h2c\r\n\r\n")
             .getBytes(StandardCharsets.UTF_8);
     private static final HeaderName HTTP2_SETTINGS_HEADER_NAME = HeaderNames.create("HTTP2-Settings");
-    private static final Base64.Decoder BASE_64_DECODER = Base64.getDecoder();
 
     private final Http2Config config;
     private final List<Http2SubProtocolSelector> subProtocolProviders;
@@ -77,8 +76,7 @@ public class Http2Upgrader implements Http1Upgrader {
                                     WritableHeaders<?> headers) {
         Http2Connection connection = new Http2Connection(ctx, config, subProtocolProviders);
         if (headers.contains(HTTP2_SETTINGS_HEADER_NAME)) {
-            connection.clientSettings(Http2Settings.create(BufferData.create(BASE_64_DECODER.decode(headers.get(
-                    HTTP2_SETTINGS_HEADER_NAME).value().getBytes(StandardCharsets.US_ASCII)))));
+            connection.clientSettings(token68ToHttp2Settings(headers.get(HTTP2_SETTINGS_HEADER_NAME).valueBytes()));
         } else {
             throw new RuntimeException("Bad request -> not " + HTTP2_SETTINGS_HEADER_NAME + " header");
         }
@@ -86,7 +84,7 @@ public class Http2Upgrader implements Http1Upgrader {
         http2Headers.path(prologue.uriPath().rawPath());
         http2Headers.method(prologue.method());
         headers.remove(HeaderNames.HOST,
-                       it -> http2Headers.authority(it.value()));
+                       it -> http2Headers.authority(it.get()));
         http2Headers.scheme("http"); // TODO need to get if https (ctx)?
 
         HttpPrologue newPrologue = HttpPrologue.create(Http2Connection.FULL_PROTOCOL,
@@ -104,4 +102,18 @@ public class Http2Upgrader implements Http1Upgrader {
         return connection;
     }
 
+    /**
+     * <a href="https://datatracker.ietf.org/doc/html/rfc7540#section-3.2.1">RFC7540 3.2.1.</a>
+     * <pre>{@code
+     * HTTP2-Settings    = token68
+     * token68           = 1*( ALPHA / DIGIT / "-" / "." / "_" / "~" / "+" / "/" ) *"="
+     * }</pre>
+     *
+     * @param bytes Base64URL encoded bytes
+     * @return HTTP/2 settings
+     */
+    private static Http2Settings token68ToHttp2Settings(byte[] bytes) {
+        return Http2Settings.create(BufferData.create(Base64.getUrlDecoder().decode(bytes)));
+    }
+
 }


Test patch:
diff --git a/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc b/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc
index 4a527b50bc..876eef808a 100644
--- a/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc
+++ b/docs/src/main/asciidoc/mp/guides/testing-junit5.adoc
@@ -177,7 +177,7 @@ This guide demonstrated how to create tests for MicroProfile applications in a J
 Refer to the following references for additional information:
 
 * https://junit.org/junit5/docs/current/user-guide/[JUnit 5 User Guide]
-* xref:../testing.adoc[Testing with JUnit 5]
+* xref:../testing/testing.adoc[Testing with JUnit 5]
 
 
 
diff --git a/docs/src/main/asciidoc/mp/testing-ng.adoc b/docs/src/main/asciidoc/mp/testing/testing-ng.adoc
similarity index 97%
rename from docs/src/main/asciidoc/mp/testing-ng.adoc
rename to docs/src/main/asciidoc/mp/testing/testing-ng.adoc
index a4f10e31ec..f21f7869bf 100644
--- a/docs/src/main/asciidoc/mp/testing-ng.adoc
+++ b/docs/src/main/asciidoc/mp/testing/testing-ng.adoc
@@ -20,7 +20,7 @@
 :description: Helidon Testing with TestNG
 :keywords: helidon, mp, test, testing, testng
 :feature-name: Testing with TestNG
-:rootdir: {docdir}/..
+:rootdir: {docdir}/../..
 
 include::{rootdir}/includes/mp.adoc[]
 
@@ -128,7 +128,7 @@ In the current example, Helidon container will be launched prior test. The _Bean
 [source,java]
 .Code sample
 ----
-include::{sourcedir}/mp/TestingNgSnippets.java[tag=snippet_1, indent=0]
+include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_1, indent=0]
 ----
 <1> Start the Helidon container.
 <2> Set disabled Bean Discovery for the current test class.
@@ -143,7 +143,7 @@ To test `@RequestScoped` bean with JaxRs support:
 [source,java]
 .Test `RequestScoped` bean
 ----
-include::{sourcedir}/mp/TestingNgSnippets.java[tag=snippet_2, indent=0]
+include::{sourcedir}/mp/testing/TestingNgSnippets.java[tag=snippet_2, indent=0]
 ----
 <1> Start the Helidon container.
 <2> Set disabled Bean discovery.
diff --git a/docs/src/main/asciidoc/mp/testing.adoc b/docs/src/main/asciidoc/mp/testing/testing.adoc
similarity index 72%
rename from docs/src/main/asciidoc/mp/testing.adoc
rename to docs/src/main/asciidoc/mp/testing/testing.adoc
index 28ff6c10a4..da0895b3fb 100644
--- a/docs/src/main/asciidoc/mp/testing.adoc
+++ b/docs/src/main/asciidoc/mp/testing/testing.adoc
@@ -22,7 +22,7 @@
 :description: Helidon Testing with JUnit5
 :keywords: helidon, mp, test, testing, junit
 :feature-name: Testing with JUnit
-:rootdir: {docdir}/..
+:rootdir: {docdir}/../..
 
 include::{rootdir}/includes/mp.adoc[]
 
@@ -32,6 +32,7 @@ include::{rootdir}/includes/mp.adoc[]
 - <<Maven Coordinates, Maven Coordinates>>
 - <<Usage, Usage>>
 - <<Examples, Examples>>
+- <<Mock Support, Mock Support>>
 - <<Additional Information, Additional Information>>
 - <<Reference, Reference>>
 
@@ -136,7 +137,7 @@ In the current example, Helidon container will be launched prior test. The _Bean
 [source,java]
 .Code sample
 ----
-include::{sourcedir}/mp/TestingSnippets.java[tag=snippet_1, indent=0]
+include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_1, indent=0]
 ----
 <1> Start the Helidon container.
 <2> Set disabled Bean Discovery for the current test class.
@@ -151,13 +152,78 @@ To test `@RequestScoped` bean with JaxRs support:
 [source,java]
 .Test `RequestScoped` bean
 ----
-include::{sourcedir}/mp/TestingSnippets.java[tag=snippet_2, indent=0]
+include::{sourcedir}/mp/testing/TestingSnippets.java[tag=snippet_2, indent=0]
 ----
 <1> Start the Helidon container.
 <2> Set disabled Bean discovery.
 <3> Add JaxRs support to the current test class.
 <4> Define a `RequestScoped` bean.
 
+== Mock Support
+
+This section describes how to mock objects using Helidon API and in a second phase, using pure CDI.
+
+=== Helidon Mock Support
+
+Helidon has its own API to use mocking with test classes annotated with `@HelidonTest`.
+
+==== Maven Coordinates
+
+To enable Helidon Mock Support add the following dependency to your project’s pom.xml.
+[source,xml]
+----
+<dependency>
+    <groupId>io.helidon.microprofile.testing</groupId>
+    <artifactId>helidon-microprofile-testing-mocking</artifactId>
+    <scope>test</scope>
+</dependency>
+----
+
+==== API
+
+It consists of one annotation named `@MockBean`, designed to be used on fields and parameters. The implementation
+relies only on CDI and thus it works with either JUnit or TestNG. The annotation has a parameter `answers` used
+to set the default answer for the mocked beans.
+
+==== Example
+
+[source,java]
+.Code sample
+----
+include::{sourcedir}/mp/testing/HelidonMockingSnippets.java[tag=snippet_1, indent=0]
+----
+<1> `service` field annotated with `@MockBean` and `Answers.CALLS_REAL_METHODS` for default answers.
+<2> Test the mocked service with real method response.
+<3> Test the mocked service with modified behavior.
+
+=== Mocking objects with pure CDI
+
+CDI can be used to enable mocking, the following example shows how to mock a service and inject it in a JAX-RS resource.
+Let's consider a simple service `FooService` with a dummy method `FooService#getFoo` that return `foo` as a `String`.
+And a resource where the service is injected and expose an endpoint at `/foo` that get the `String` provided by the service.
+
+[source,java]
+.Code sample
+----
+include::{sourcedir}/mp/testing/CDIMockingSnippets.java[tag=snippet_1, indent=0]
+----
+<1> A simple `foo` Service.
+<2> Inject the service into the resource.
+
+This example uses Mockito to create mock instances before each test. The method mockFooService is a CDI producer method
+that adds the mock instance as an @Alternative in order to replace the FooService singleton.
+
+[source,java]
+.Code sample
+----
+include::{sourcedir}/mp/testing/CDIMockingSnippets.java[tag=snippet_2, indent=0]
+----
+<1> Set priority to 1 because of `@Alternative`.
+<2> Set `fooService` to a new mock before each tests.
+<3> This makes `FooResource` inject the mock instead of the default singleton.
+<4> Test that the mock is injected with modified behavior.
+<5> Test the real method behavior.
+
 == Additional Information
 
 * https://medium.com/helidon/testing-helidon-9df2ea14e22[Official blog article about Helidon and JUnit usage]
diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/CDIMockingSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/CDIMockingSnippets.java
new file mode 100644
index 0000000000..a0841f7edc
--- /dev/null
+++ b/docs/src/main/java/io/helidon/docs/mp/testing/CDIMockingSnippets.java
@@ -0,0 +1,104 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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
+ *
+ *     http://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 io.helidon.docs.mp.testing;
+
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+
+import jakarta.annotation.Priority;
+import jakarta.enterprise.context.ApplicationScoped;
+import jakarta.enterprise.inject.Alternative;
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.Produces;
+import jakarta.ws.rs.client.WebTarget;
+import jakarta.ws.rs.core.Response;
+
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
+import org.mockito.Mockito;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.when;
+
+@SuppressWarnings("ALL")
+class CDIMockingSnippets {
+
+    // tag::snippet_1[]
+    @ApplicationScoped
+    public class FooService { // <1>
+
+        public String getFoo() {
+            return "foo";
+        }
+    }
+
+    @Path("/foo")
+    public class FooResource {
+
+        @Inject
+        private FooService fooService; // <2>
+
+        @GET
+        public String getFoo() {
+            return fooService.getFoo();
+        }
+    }
+    // end::snippet_1[]
+
+    // tag::snippet_2[]
+    @HelidonTest
+    @Priority(1) // <1>
+    class FooTest {
+
+        @Inject
+        private WebTarget target;
+
+        private FooService fooService;
+
+        @BeforeEach
+        void initMock() {
+            fooService = Mockito.mock(FooService.class, Answers.CALLS_REAL_METHODS); // <2>
+        }
+
+        @Produces
+        @Alternative
+        FooService mockFooService() {
+            return fooService; // <3>
+        }
+
+        @Test
+        void testMockedService() {
+            when(fooService.getFoo()).thenReturn("bar"); // <4>
+
+            Response response = target.path("/foo").request().get();
+
+            assertThat(response.getStatus(), is(200));
+            assertThat(response.readEntity(String.class), is("bar"));
+        }
+
+        @Test
+        void testService() {
+            Response response = target.path("/foo").request().get(); // <5>
+
+            assertThat(response.getStatus(), is(200));
+            assertThat(response.readEntity(String.class), is("foo"));
+        }
+    }
+    // end::snippet_2[]
+}
diff --git a/docs/src/main/java/io/helidon/docs/mp/testing/HelidonMockingSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/HelidonMockingSnippets.java
new file mode 100644
index 0000000000..0ad2cb5cbb
--- /dev/null
+++ b/docs/src/main/java/io/helidon/docs/mp/testing/HelidonMockingSnippets.java
@@ -0,0 +1,78 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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
+ *
+ *     http://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 io.helidon.docs.mp.testing;
+
+import io.helidon.microprofile.testing.junit5.AddBean;
+import io.helidon.microprofile.testing.junit5.HelidonTest;
+import io.helidon.microprofile.testing.mocking.MockBean;
+
+import jakarta.inject.Inject;
+import jakarta.ws.rs.GET;
+import jakarta.ws.rs.Path;
+import jakarta.ws.rs.client.WebTarget;
+
+import org.junit.jupiter.api.Test;
+import org.mockito.Answers;
+import org.mockito.Mockito;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+
+@SuppressWarnings("ALL")
+class HelidonMockingSnippets {
+
+    // tag::snippet_1[]
+    @HelidonTest
+    @AddBean(MockBeanAnswerTest.Resource.class)
+    @AddBean(MockBeanAnswerTest.Service.class)
+    class MockBeanAnswerTest {
+
+        @MockBean(answer = Answers.CALLS_REAL_METHODS) // <1>
+        private Service service;
+        @Inject
+        private WebTarget target;
+
+        @Test
+        void injectionTest() {
+            String response = target.path("/test").request().get(String.class);
+            assertThat(response, is("Not Mocked")); // <2>
+            Mockito.when(service.test()).thenReturn("Mocked");
+            response = target.path("/test").request().get(String.class);
+            assertThat(response, is("Mocked")); // <3>
+        }
+
+        @Path("/test")
+        public static class Resource {
+
+            @Inject
+            private Service service;
+
+            @GET
+            public String test() {
+                return service.test();
+            }
+        }
+
+        static class Service {
+
+            String test() {
+                return "Not Mocked";
+            }
+
+        }
+    }
+    // end::snippet_1[]
+}
diff --git a/docs/src/main/java/io/helidon/docs/mp/TestingNgSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java
similarity index 98%
rename from docs/src/main/java/io/helidon/docs/mp/TestingNgSnippets.java
rename to docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java
index f39734ef1c..d696e44cad 100644
--- a/docs/src/main/java/io/helidon/docs/mp/TestingNgSnippets.java
+++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingNgSnippets.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.helidon.docs.mp;
+package io.helidon.docs.mp.testing;
 
 import io.helidon.microprofile.config.ConfigCdiExtension;
 import io.helidon.microprofile.testing.testng.AddBean;
diff --git a/docs/src/main/java/io/helidon/docs/mp/TestingSnippets.java b/docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java
similarity index 98%
rename from docs/src/main/java/io/helidon/docs/mp/TestingSnippets.java
rename to docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java
index a7dfc2d39f..919d2062a2 100644
--- a/docs/src/main/java/io/helidon/docs/mp/TestingSnippets.java
+++ b/docs/src/main/java/io/helidon/docs/mp/testing/TestingSnippets.java
@@ -13,7 +13,7 @@
  * See the License for the specific language governing permissions and
  * limitations under the License.
  */
-package io.helidon.docs.mp;
+package io.helidon.docs.mp.testing;
 
 import io.helidon.microprofile.config.ConfigCdiExtension;
 import io.helidon.microprofile.testing.junit5.AddBean;
diff --git a/tracing/provider-tests/src/main/java/io/helidon/tracing/providers/tests/TestTracerAndSpanPropagation.java b/tracing/provider-tests/src/main/java/io/helidon/tracing/providers/tests/TestTracerAndSpanPropagation.java
new file mode 100644
index 0000000000..32b9524f3e
--- /dev/null
+++ b/tracing/provider-tests/src/main/java/io/helidon/tracing/providers/tests/TestTracerAndSpanPropagation.java
@@ -0,0 +1,137 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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
+ *
+ *     http://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 io.helidon.tracing.providers.tests;
+
+import java.util.Objects;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import io.helidon.common.context.Context;
+import io.helidon.common.context.Contexts;
+import io.helidon.common.testing.junit5.OptionalMatcher;
+import io.helidon.tracing.Span;
+import io.helidon.tracing.SpanContext;
+import io.helidon.tracing.Tracer;
+
+import org.junit.jupiter.api.Test;
+
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.sameInstance;
+
+/**
+ * Tests that the current tracer and span are propagated through Helidon contexts.
+ * <p>
+ * Derived as closely as possible from a user-supplied reproducer. Basically, logging was changed to use system
+ * logging instead of SLF4J as in the original test and test assertions were converted to use Hamcrest. The key logic
+ * in the test is unchanged.
+ * </p>
+ */
+class TestTracerAndSpanPropagation {
+    private static final System.Logger LOGGER = System
+            .getLogger(TestTracerAndSpanPropagation.class.getName());
+
+    @Test
+    void concurrentVirtualThreadUseCanary() {
+        Contexts.runInContext(Context.create(), this::actualTest);
+    }
+
+    private void actualTest() {
+
+        final var tracer = buildTracer();
+        LOGGER.log(System.Logger.Level.INFO, "Tracer {0}", tracer);
+        assertThat("Tracer is enabled", tracer.enabled(), is(true));
+        assertThat("Tracer in use is the global tracer", tracer, sameInstance(Tracer.global()));
+        final var rootSpan = tracer.spanBuilder(getClass().getSimpleName()).start();
+        LOGGER.log(System.Logger.Level.INFO, "traceId: {0}", rootSpan.context().traceId());
+        try (var ignored = rootSpan.activate()) {
+            assertThat("rootSpan activate",
+                       Span.current().map(Span::context).map(SpanContext::spanId),
+                       OptionalMatcher.optionalValue(is(rootSpan.context().spanId())));
+
+            final var ff = new CompletableFuture[1];
+            try (var executor = Contexts.wrap(Executors.newVirtualThreadPerTaskExecutor())) {
+                final var futures = new CompletableFuture[5];
+                for (int i = 0; i < 5; i++) {
+                    futures[i] = CompletableFuture
+                            .runAsync(new ChildAction(tracer, rootSpan), executor);
+                }
+                for (final var f : futures) {
+                    ff[0] = f;
+                    f.get(1, TimeUnit.SECONDS);
+                }
+                LOGGER.log(System.Logger.Level.INFO, "all futures complete");
+
+            } catch (ExecutionException | TimeoutException | InterruptedException e) {
+                LOGGER.log(System.Logger.Level.ERROR, "Failure: f={0}", ff[0], e);
+                rootSpan.end(e);
+                throw new RuntimeException(e);
+            }
+            rootSpan.end();
+            LOGGER.log(System.Logger.Level.INFO, "ended rootSpan");
+        }
+        try {
+            Thread.sleep(100);
+        } catch (InterruptedException e) {
+            LOGGER.log(System.Logger.Level.ERROR, "Error on sleep", e);
+            throw new RuntimeException(e);
+        }
+        LOGGER.log(System.Logger.Level.INFO, "test ended");
+    }
+
+    private Tracer buildTracer() {
+        return Tracer.global();
+    }
+
+    record ChildAction(Tracer globalTracer, Span rootSpan) implements Runnable {
+
+        @Override
+        public void run() {
+            final var thread = Thread.currentThread();
+            assertThat("isVirtual false in ChildAction", thread.isVirtual(), is(true));
+            final var threadName = String.valueOf(thread);
+            LOGGER.log(System.Logger.Level.INFO, "Running {0}", threadName);
+
+            final var tracer = Objects.requireNonNull(Tracer.global(), "global NOT in ChildAction");
+            assertThat("tracer NOT preserved in ChildAction", tracer, is(sameInstance(globalTracer)));
+            assertThat("Current span from test NOT present in ChildAction",
+                       Span.current().map(Span::context).map(SpanContext::spanId),
+                       OptionalMatcher.optionalValue(is(rootSpan.context().spanId())));
+
+            final var span = Span.current().get();
+            final var spanContext = span.context();
+            final var childSpan = tracer.spanBuilder(threadName)
+                    .parent(spanContext).start();
+            try (var ignored = childSpan.activate()) {
+
+                assertThat("childSpan NOT activated",
+                           Span.current().map(Span::context).map(SpanContext::spanId),
+                           OptionalMatcher.optionalValue(is(childSpan.context().spanId())));
+                Thread.sleep(10);
+                span.end();
+
+            } catch (InterruptedException e) {
+                span.end(e);
+                throw new RuntimeException(e);
+            } finally {
+                LOGGER.log(System.Logger.Level.INFO, "Ended {0}", threadName);
+            }
+        }
+    }
+}
diff --git a/tracing/provider-tests/src/main/java/module-info.java b/tracing/provider-tests/src/main/java/module-info.java
index 8a90d89376..dd4c2b9701 100644
--- a/tracing/provider-tests/src/main/java/module-info.java
+++ b/tracing/provider-tests/src/main/java/module-info.java
@@ -21,6 +21,7 @@ module io.helidon.tracing.provider.tests {
 
     requires java.logging;
     requires io.helidon.tracing;
+    requires io.helidon.common.context;
     requires io.helidon.common.testing.junit5;
 
     requires org.junit.jupiter.api;
diff --git a/webserver/http2/src/test/java/io/helidon/webserver/http2/UpgradeSettingsTest.java b/webserver/http2/src/test/java/io/helidon/webserver/http2/UpgradeSettingsTest.java
new file mode 100644
index 0000000000..409350b646
--- /dev/null
+++ b/webserver/http2/src/test/java/io/helidon/webserver/http2/UpgradeSettingsTest.java
@@ -0,0 +1,108 @@
+/*
+ * Copyright (c) 2024 Oracle and/or its affiliates.
+ *
+ * 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
+ *
+ *     http://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 io.helidon.webserver.http2;
+
+import java.util.Base64;
+
+import io.helidon.common.buffers.BufferData;
+import io.helidon.common.buffers.DataWriter;
+import io.helidon.http.HeaderValues;
+import io.helidon.http.HttpPrologue;
+import io.helidon.http.Method;
+import io.helidon.http.WritableHeaders;
+import io.helidon.http.http2.Http2Flag;
+import io.helidon.http.http2.Http2Settings;
+import io.helidon.webserver.ConnectionContext;
+import io.helidon.webserver.ListenerContext;
+import io.helidon.webserver.Router;
+
+import org.junit.jupiter.api.Test;
+
+import static io.helidon.http.http2.Http2Setting.ENABLE_PUSH;
+import static io.helidon.http.http2.Http2Setting.HEADER_TABLE_SIZE;
+import static io.helidon.http.http2.Http2Setting.INITIAL_WINDOW_SIZE;
+import static io.helidon.http.http2.Http2Setting.MAX_CONCURRENT_STREAMS;
+import static io.helidon.http.http2.Http2Setting.MAX_FRAME_SIZE;
+import static io.helidon.http.http2.Http2Setting.MAX_HEADER_LIST_SIZE;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+class UpgradeSettingsTest {
+
+    static final long MAX_UNSIGNED_INT = 0xFFFFFFFFL;
+
+    private final ConnectionContext ctx;
+    private final HttpPrologue prologue;
+
+    public UpgradeSettingsTest() {
+        ctx = mock(ConnectionContext.class);
+        prologue = HttpPrologue.create("http/1.1",
+                                       "http",
+                                       "1.1",
+                                       Method.GET,
+                                       "/resource.txt",
+                                       false);
+        DataWriter dataWriter = mock(DataWriter.class);
+        when(ctx.router()).thenReturn(Router.empty());
+        when(ctx.listenerContext()).thenReturn(mock(ListenerContext.class));
+        when(ctx.dataWriter()).thenReturn(dataWriter);
+    }
+
+    @Test
+    void urlEncodedSettingsGH8399() {
+        Http2Settings s = upgrade("AAEAABAAAAIAAAABAAN_____AAQAAP__AAUAAEAAAAYAACAA");
+        assertThat(s.presentValue(HEADER_TABLE_SIZE).orElseThrow(), is(4096L));
+        assertThat(s.presentValue(ENABLE_PUSH).orElseThrow(), is(true));
+        assertThat(s.presentValue(MAX_CONCURRENT_STREAMS).orElseThrow(), is(MAX_UNSIGNED_INT / 2));
+        assertThat(s.presentValue(INITIAL_WINDOW_SIZE).orElseThrow(), is(65_535L));
+        assertThat(s.presentValue(MAX_FRAME_SIZE).orElseThrow(), is(16_384L));
+        assertThat(s.presentValue(MAX_HEADER_LIST_SIZE).orElseThrow(), is(8192L));
+    }
+
+    @Test
+    void urlEncodedSettings() {
+        Http2Settings settings2 = Http2Settings.builder()
+                .add(HEADER_TABLE_SIZE, 4096L)
+                .add(ENABLE_PUSH, false)
+                .add(MAX_CONCURRENT_STREAMS, MAX_UNSIGNED_INT - 5)
+                .add(INITIAL_WINDOW_SIZE, 65535L)
+                .add(MAX_FRAME_SIZE, 16384L)
+                .add(MAX_HEADER_LIST_SIZE, 256L)
+                .build();
+        String encSett = Base64.getUrlEncoder().encodeToString(settingsToBytes(settings2));
+        Http2Settings s = upgrade(encSett);
+        assertThat(s.presentValue(MAX_CONCURRENT_STREAMS).orElseThrow(), is(MAX_UNSIGNED_INT - 5));
+        assertThat(s.presentValue(MAX_HEADER_LIST_SIZE).orElseThrow(), is(256L));
+    }
+
+    Http2Settings upgrade(String http2Settings) {
+        WritableHeaders<?> headers = WritableHeaders.create().add(HeaderValues.create("HTTP2-Settings", http2Settings));
+        Http2Upgrader http2Upgrader = Http2Upgrader.create(Http2Config.create());
+        Http2Connection connection = (Http2Connection) http2Upgrader.upgrade(ctx, prologue, headers);
+        return connection.clientSettings();
+    }
+
+    byte[] settingsToBytes(Http2Settings settings) {
+        BufferData settingsFrameData =
+                settings.toFrameData(null, 0, Http2Flag.SettingsFlags.create(0)).data();
+        byte[] b = new byte[settingsFrameData.available()];
+        settingsFrameData.read(b);
+        return b;
+    }
+}