diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamHandler.java
new file mode 100644
index 0000000000..c9c27c074d
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamHandler.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
+import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
+import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientH2StreamHandler {
+
+ private ClientH2StreamHandler newHandler() {
+ final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics(
+ new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics());
+ final AsyncClientExchangeHandler exchangeHandler = Mockito.mock(AsyncClientExchangeHandler.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ClientH2StreamHandler(channel, httpProcessor, metrics, exchangeHandler, pushHandlerFactory,
+ HttpCoreContext.create());
+ }
+
+ @Test
+ void defaults() {
+ final ClientH2StreamHandler handler = newHandler();
+ Assertions.assertNotNull(handler.getPushHandlerFactory());
+ Assertions.assertTrue(handler.isOutputReady());
+ }
+
+ @Test
+ void consumePromiseRejected() {
+ final ClientH2StreamHandler handler = newHandler();
+ Assertions.assertThrows(ProtocolException.class, () -> handler.consumePromise(null));
+ }
+
+ @Test
+ void consumeDataRejectedBeforeHeaders() {
+ final ClientH2StreamHandler handler = newHandler();
+ Assertions.assertThrows(ProtocolException.class, () ->
+ handler.consumeData(ByteBuffer.allocate(0), true));
+ }
+
+ @Test
+ void updateCapacityDelegates() throws Exception {
+ final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics(
+ new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics());
+ final AsyncClientExchangeHandler exchangeHandler = Mockito.mock(AsyncClientExchangeHandler.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ClientH2StreamHandler handler = new ClientH2StreamHandler(
+ channel, httpProcessor, metrics, exchangeHandler, pushHandlerFactory, HttpCoreContext.create());
+
+ handler.updateInputCapacity();
+
+ Mockito.verify(exchangeHandler).updateCapacity(channel);
+ }
+
+ @Test
+ void toStringIncludesStates() {
+ final ClientH2StreamHandler handler = newHandler();
+ final String text = handler.toString();
+ Assertions.assertTrue(text.contains("requestState"));
+ Assertions.assertTrue(text.contains("responseState"));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamMultiplexer.java
new file mode 100644
index 0000000000..b38ffc8640
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamMultiplexer.java
@@ -0,0 +1,130 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.AsyncPushProducer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.H2ConnectionException;
+import org.apache.hc.core5.http2.H2Error;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.http2.config.H2Param;
+import org.apache.hc.core5.http2.frame.DefaultFrameFactory;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientH2StreamMultiplexer {
+
+ private ClientH2StreamMultiplexer newMultiplexer() {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ClientH2StreamMultiplexer(
+ ioSession,
+ DefaultFrameFactory.INSTANCE,
+ httpProcessor,
+ pushHandlerFactory,
+ H2Config.DEFAULT,
+ null,
+ null);
+ }
+
+ @Test
+ void validateSettingRejectsEnablePush() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ final H2ConnectionException ex = Assertions.assertThrows(H2ConnectionException.class, () ->
+ multiplexer.validateSetting(H2Param.ENABLE_PUSH, 1));
+ Assertions.assertEquals(H2Error.PROTOCOL_ERROR.getCode(), ex.getCode());
+ }
+
+ @Test
+ void acceptHeaderFrameRejected() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ Assertions.assertThrows(H2ConnectionException.class, multiplexer::acceptHeaderFrame);
+ }
+
+ @Test
+ void outgoingRequestCreatesHandler() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ final H2StreamHandler handler = multiplexer.outgoingRequest(
+ Mockito.mock(H2StreamChannel.class),
+ Mockito.mock(AsyncClientExchangeHandler.class),
+ null,
+ null);
+ Assertions.assertNotNull(handler);
+ }
+
+ @Test
+ void incomingRequestRejected() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ Assertions.assertThrows(H2ConnectionException.class, () ->
+ multiplexer.incomingRequest(Mockito.mock(H2StreamChannel.class)));
+ }
+
+ @Test
+ void outgoingPushPromiseRejected() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ Assertions.assertThrows(H2ConnectionException.class, () ->
+ multiplexer.outgoingPushPromise(Mockito.mock(H2StreamChannel.class), Mockito.mock(AsyncPushProducer.class)));
+ }
+
+ @Test
+ void incomingPushPromiseCreatesHandler() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final H2StreamHandler handler = multiplexer.incomingPushPromise(
+ Mockito.mock(H2StreamChannel.class),
+ pushHandlerFactory);
+ Assertions.assertNotNull(handler);
+ }
+
+ @Test
+ void allowGracefulAbortUsesRemoteClosed() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ final H2Stream stream = Mockito.mock(H2Stream.class);
+ Mockito.when(stream.isRemoteClosed()).thenReturn(true);
+ Mockito.when(stream.isLocalClosed()).thenReturn(false);
+ Assertions.assertTrue(multiplexer.allowGracefulAbort(stream));
+ }
+
+ @Test
+ void toStringContainsState() {
+ final ClientH2StreamMultiplexer multiplexer = newMultiplexer();
+ final String text = multiplexer.toString();
+ Assertions.assertTrue(text.startsWith("["));
+ Assertions.assertTrue(text.endsWith("]"));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamMultiplexerFactory.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamMultiplexerFactory.java
new file mode 100644
index 0000000000..fe8a0484e5
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2StreamMultiplexerFactory.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientH2StreamMultiplexerFactory {
+
+ @Test
+ void createUsesDefaults() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ClientH2StreamMultiplexerFactory factory = new ClientH2StreamMultiplexerFactory(
+ httpProcessor, pushHandlerFactory, null, null, null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final ClientH2StreamMultiplexer multiplexer = factory.create(ioSession);
+
+ Assertions.assertNotNull(multiplexer);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2UpgradeHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2UpgradeHandler.java
new file mode 100644
index 0000000000..6516ca3245
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientH2UpgradeHandler.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientH2UpgradeHandler {
+
+ @Test
+ void upgradeRegistersPrefaceHandler() throws Exception {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ClientH2StreamMultiplexerFactory factory = new ClientH2StreamMultiplexerFactory(
+ httpProcessor, pushHandlerFactory, null, null, null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ Mockito.when(ioSession.getLock()).thenReturn(new ReentrantLock());
+
+ final ClientH2UpgradeHandler handler = new ClientH2UpgradeHandler(factory, (Callback) ex -> {
+ });
+ handler.upgrade(ioSession, null);
+
+ Mockito.verify(ioSession).upgrade(Mockito.any());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttp1UpgradeHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttp1UpgradeHandler.java
new file mode 100644
index 0000000000..83bf78e2fd
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttp1UpgradeHandler.java
@@ -0,0 +1,74 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.config.CharCodingConfig;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.impl.nio.ClientHttp1StreamDuplexerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientHttp1UpgradeHandler {
+
+ @Test
+ void upgradeCreatesHandlerAndCompletesCallback() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ final ClientHttp1StreamDuplexerFactory factory = new ClientHttp1StreamDuplexerFactory(
+ httpProcessor, Http1Config.DEFAULT, CharCodingConfig.DEFAULT);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ Mockito.when(ioSession.poll()).thenReturn(null);
+
+ final ClientHttp1UpgradeHandler handler = new ClientHttp1UpgradeHandler(factory);
+ final AtomicReference completed = new AtomicReference<>();
+
+ handler.upgrade(ioSession, new FutureCallback() {
+ @Override
+ public void completed(final ProtocolIOSession result) {
+ completed.set(result);
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ }
+
+ @Override
+ public void cancelled() {
+ }
+ });
+
+ Assertions.assertSame(ioSession, completed.get());
+ Mockito.verify(ioSession).upgrade(Mockito.any());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java
new file mode 100644
index 0000000000..c0c6101bcc
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientHttpProtocolNegotiationStarter.java
@@ -0,0 +1,119 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.config.CharCodingConfig;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.impl.nio.ClientHttp1IOEventHandler;
+import org.apache.hc.core5.http.impl.nio.ClientHttp1StreamDuplexerFactory;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.http2.ssl.ApplicationProtocol;
+import org.apache.hc.core5.reactor.EndpointParameters;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientHttpProtocolNegotiationStarter {
+
+ private static ClientHttp1StreamDuplexerFactory http1Factory() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ return new ClientHttp1StreamDuplexerFactory(httpProcessor, Http1Config.DEFAULT, CharCodingConfig.DEFAULT);
+ }
+
+ private static ClientH2StreamMultiplexerFactory http2Factory() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ClientH2StreamMultiplexerFactory(httpProcessor, pushHandlerFactory, null, null, null);
+ }
+
+ @Test
+ void forceHttp2UsesPrefaceHandlerAndTlsUpgrade() {
+ final TlsStrategy tlsStrategy = Mockito.mock(TlsStrategy.class);
+ final ClientHttpProtocolNegotiationStarter starter = new ClientHttpProtocolNegotiationStarter(
+ http1Factory(),
+ http2Factory(),
+ HttpVersionPolicy.FORCE_HTTP_2,
+ tlsStrategy,
+ Timeout.ofSeconds(1),
+ (Callback) ex -> {
+ });
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final EndpointParameters endpoint = new EndpointParameters(URIScheme.HTTPS.id, "localhost", 8443,
+ HttpVersionPolicy.FORCE_HTTP_2);
+
+ final Object handler = starter.createHandler(ioSession, endpoint);
+
+ Assertions.assertTrue(handler instanceof ClientH2PrefaceHandler);
+ Mockito.verify(ioSession).registerProtocol(Mockito.eq(ApplicationProtocol.HTTP_1_1.id), Mockito.any());
+ Mockito.verify(ioSession).registerProtocol(Mockito.eq(ApplicationProtocol.HTTP_2.id), Mockito.any());
+ Mockito.verify(tlsStrategy).upgrade(ioSession, endpoint, HttpVersionPolicy.FORCE_HTTP_2, Timeout.ofSeconds(1), null);
+ }
+
+ @Test
+ void forceHttp1UsesHttp1Handler() {
+ final ClientHttpProtocolNegotiationStarter starter = new ClientHttpProtocolNegotiationStarter(
+ http1Factory(),
+ http2Factory(),
+ HttpVersionPolicy.FORCE_HTTP_1,
+ null,
+ null,
+ null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final Object handler = starter.createHandler(ioSession, null);
+
+ Assertions.assertTrue(handler instanceof ClientHttp1IOEventHandler);
+ }
+
+ @Test
+ void negotiateUsesProtocolNegotiator() {
+ final ClientHttpProtocolNegotiationStarter starter = new ClientHttpProtocolNegotiationStarter(
+ http1Factory(),
+ http2Factory(),
+ HttpVersionPolicy.NEGOTIATE,
+ null,
+ null,
+ null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final Object handler = starter.createHandler(ioSession, null);
+
+ Assertions.assertTrue(handler instanceof HttpProtocolNegotiator);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java
new file mode 100644
index 0000000000..2b6c42974f
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestClientPushH2StreamHandler.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
+import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
+import org.apache.hc.core5.http.message.BasicHeader;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.H2Error;
+import org.apache.hc.core5.http2.H2StreamResetException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestClientPushH2StreamHandler {
+
+ private ClientPushH2StreamHandler newHandler(final HandlerFactory pushHandlerFactory) {
+ final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics(
+ new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics());
+ return new ClientPushH2StreamHandler(channel, httpProcessor, metrics, pushHandlerFactory,
+ HttpCoreContext.create());
+ }
+
+ @Test
+ void consumePromiseRefusedWhenFactoryReturnsNull() throws Exception {
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ Mockito.when(pushHandlerFactory.create(Mockito.any(), Mockito.any())).thenReturn(null);
+ final ClientPushH2StreamHandler handler = newHandler(pushHandlerFactory);
+
+ final H2StreamResetException ex = Assertions.assertThrows(H2StreamResetException.class, () ->
+ handler.consumePromise(java.util.Arrays.asList(
+ new BasicHeader(":method", "GET"),
+ new BasicHeader(":scheme", "https"),
+ new BasicHeader(":authority", "example.com"),
+ new BasicHeader(":path", "/"))));
+ Assertions.assertEquals(H2Error.REFUSED_STREAM.getCode(), ex.getCode());
+ }
+
+ @Test
+ void consumeDataRejectedBeforeBody() {
+ final ClientPushH2StreamHandler handler = newHandler(null);
+ Assertions.assertThrows(ProtocolException.class, () ->
+ handler.consumeData(ByteBuffer.allocate(0), true));
+ }
+
+ @Test
+ void updateCapacityFailsWithoutHandler() {
+ final ClientPushH2StreamHandler handler = newHandler(null);
+ Assertions.assertThrows(IllegalStateException.class, handler::updateInputCapacity);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestNoopAsyncPushHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestNoopAsyncPushHandler.java
new file mode 100644
index 0000000000..1c15cc0d2c
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestNoopAsyncPushHandler.java
@@ -0,0 +1,59 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+
+import org.apache.hc.core5.http.nio.CapacityChannel;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestNoopAsyncPushHandler {
+
+ @Test
+ void updateCapacityAllowsAnyPayload() throws Exception {
+ final CapacityChannel capacityChannel = Mockito.mock(CapacityChannel.class);
+ final NoopAsyncPushHandler handler = new NoopAsyncPushHandler();
+
+ handler.updateCapacity(capacityChannel);
+
+ Mockito.verify(capacityChannel).update(Integer.MAX_VALUE);
+ }
+
+ @Test
+ void noOpsDoNotThrow() throws Exception {
+ final NoopAsyncPushHandler handler = new NoopAsyncPushHandler();
+
+ Assertions.assertDoesNotThrow(() -> handler.consume(ByteBuffer.allocate(0)));
+ Assertions.assertDoesNotThrow(() -> handler.streamEnd(Collections.emptyList()));
+ Assertions.assertDoesNotThrow(() -> handler.failed(new RuntimeException("boom")));
+ Assertions.assertDoesNotThrow(handler::releaseResources);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestNoopH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestNoopH2StreamHandler.java
new file mode 100644
index 0000000000..2b6573e86d
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestNoopH2StreamHandler.java
@@ -0,0 +1,57 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+import java.util.Collections;
+
+import org.apache.hc.core5.http.HttpException;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestNoopH2StreamHandler {
+
+ @Test
+ void singletonDefaults() {
+ Assertions.assertSame(NoopH2StreamHandler.INSTANCE, NoopH2StreamHandler.INSTANCE);
+ Assertions.assertFalse(NoopH2StreamHandler.INSTANCE.isOutputReady());
+ Assertions.assertNull(NoopH2StreamHandler.INSTANCE.getPushHandlerFactory());
+ }
+
+ @Test
+ void noOpsDoNotThrow() throws Exception {
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.produceOutput());
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.consumePromise(Collections.emptyList()));
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.consumeHeader(Collections.emptyList(), false));
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.updateInputCapacity());
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.consumeData(ByteBuffer.allocate(0), true));
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.failed(new RuntimeException("boom")));
+ Assertions.assertDoesNotThrow(() -> NoopH2StreamHandler.INSTANCE.handle(new HttpException("oops"), true));
+ Assertions.assertDoesNotThrow(NoopH2StreamHandler.INSTANCE::releaseResources);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestPrefaceHandlerBase.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestPrefaceHandlerBase.java
new file mode 100644
index 0000000000..9bd54cf13e
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestPrefaceHandlerBase.java
@@ -0,0 +1,170 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.net.InetSocketAddress;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.ssl.SSLSession;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.http.ConnectionClosedException;
+import org.apache.hc.core5.http.ProtocolVersion;
+import org.apache.hc.core5.http.impl.nio.HttpConnectionEventHandler;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.IOSession;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.apache.hc.core5.reactor.ssl.TlsDetails;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestPrefaceHandlerBase {
+
+ private static class TestHandler extends PrefaceHandlerBase {
+ TestHandler(final ProtocolIOSession ioSession,
+ final FutureCallback resultCallback,
+ final Callback exceptionCallback) {
+ super(ioSession, resultCallback, exceptionCallback);
+ }
+
+ @Override
+ public void connected(final IOSession session) {
+ }
+
+ @Override
+ public void inputReady(final IOSession session, final ByteBuffer buffer) {
+ }
+
+ @Override
+ public void outputReady(final IOSession session) {
+ }
+ }
+
+ @Test
+ void startProtocolUpgradesAndSignals() throws Exception {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ @SuppressWarnings("unchecked")
+ final FutureCallback resultCallback =
+ (FutureCallback) Mockito.mock(FutureCallback.class);
+ try (TestHandler handler = new TestHandler(ioSession, resultCallback, null)) {
+
+ final HttpConnectionEventHandler protocolHandler = Mockito.mock(HttpConnectionEventHandler.class);
+ final ByteBuffer data = ByteBuffer.wrap(new byte[] {1, 2, 3});
+
+ handler.startProtocol(protocolHandler, data);
+
+ Mockito.verify(ioSession).upgrade(protocolHandler);
+ Mockito.verify(protocolHandler).connected(ioSession);
+ Mockito.verify(protocolHandler).inputReady(ioSession, data);
+ Mockito.verify(resultCallback).completed(ioSession);
+ }
+ }
+
+ @Test
+ void exceptionDelegatesToProtocolHandler() throws Exception {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ try (TestHandler handler = new TestHandler(ioSession, null, null)) {
+ final HttpConnectionEventHandler protocolHandler = Mockito.mock(HttpConnectionEventHandler.class);
+
+ handler.startProtocol(protocolHandler, null);
+ final RuntimeException cause = new RuntimeException("boom");
+ handler.exception(ioSession, cause);
+
+ Mockito.verify(ioSession).close(CloseMode.IMMEDIATE);
+ Mockito.verify(protocolHandler).exception(Mockito.eq(ioSession), Mockito.eq(cause));
+ }
+ }
+
+ @Test
+ void disconnectedWithoutProtocolHandlerFailsCallback() throws Exception {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final AtomicReference failed = new AtomicReference<>();
+ final FutureCallback callback = new FutureCallback() {
+ @Override
+ public void completed(final ProtocolIOSession result) {
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ failed.set(ex);
+ }
+
+ @Override
+ public void cancelled() {
+ }
+ };
+ try (TestHandler handler = new TestHandler(ioSession, callback, null)) {
+
+ handler.disconnected(ioSession);
+
+ Assertions.assertTrue(failed.get() instanceof ConnectionClosedException);
+ }
+ }
+
+ @Test
+ void sslSessionIsExposed() throws Exception {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final SSLSession sslSession = Mockito.mock(SSLSession.class);
+ final TlsDetails tlsDetails = new TlsDetails(sslSession, "h2");
+ Mockito.when(ioSession.getTlsDetails()).thenReturn(tlsDetails);
+
+ try (TestHandler handler = new TestHandler(ioSession, null, null)) {
+
+ Assertions.assertSame(sslSession, handler.getSSLSession());
+ }
+ }
+
+ @Test
+ void delegatesSessionProperties() throws Exception {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ Mockito.when(ioSession.getSocketTimeout()).thenReturn(Timeout.ofSeconds(2));
+ Mockito.when(ioSession.getRemoteAddress()).thenReturn(new InetSocketAddress("localhost", 80));
+ Mockito.when(ioSession.getLocalAddress()).thenReturn(new InetSocketAddress("localhost", 0));
+ Mockito.when(ioSession.isOpen()).thenReturn(true);
+
+ try (TestHandler handler = new TestHandler(ioSession, null, null)) {
+
+ Assertions.assertEquals(Timeout.ofSeconds(2), handler.getSocketTimeout());
+ handler.setSocketTimeout(Timeout.ofSeconds(1));
+ Mockito.verify(ioSession).setSocketTimeout(Timeout.ofSeconds(1));
+ Assertions.assertEquals(new InetSocketAddress("localhost", 80), handler.getRemoteAddress());
+ Assertions.assertEquals(new InetSocketAddress("localhost", 0), handler.getLocalAddress());
+ Assertions.assertTrue(handler.isOpen());
+ final ProtocolVersion protocolVersion = handler.getProtocolVersion();
+ Assertions.assertNotNull(protocolVersion);
+ handler.close();
+ handler.close(CloseMode.GRACEFUL);
+ Mockito.verify(ioSession).close();
+ Mockito.verify(ioSession).close(CloseMode.GRACEFUL);
+ }
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestProtocolNegotiationException.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestProtocolNegotiationException.java
new file mode 100644
index 0000000000..090a390b8f
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestProtocolNegotiationException.java
@@ -0,0 +1,43 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestProtocolNegotiationException {
+
+ @Test
+ void messageIsPreserved() {
+ final ProtocolNegotiationException ex = new ProtocolNegotiationException("nope");
+ Assertions.assertEquals("nope", ex.getMessage());
+ Assertions.assertTrue(ex instanceof IOException);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2IOEventHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2IOEventHandler.java
new file mode 100644
index 0000000000..c81ffbebc1
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2IOEventHandler.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.net.InetSocketAddress;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerH2IOEventHandler {
+
+ @Test
+ void toStringIncludesState() throws Exception {
+ final ServerH2StreamMultiplexer multiplexer = Mockito.mock(ServerH2StreamMultiplexer.class);
+ Mockito.when(multiplexer.getRemoteAddress()).thenReturn(new InetSocketAddress("localhost", 9443));
+ Mockito.when(multiplexer.getLocalAddress()).thenReturn(new InetSocketAddress("localhost", 8443));
+ Mockito.doAnswer(invocation -> {
+ final StringBuilder builder = invocation.getArgument(0);
+ builder.append("state");
+ return null;
+ }).when(multiplexer).appendState(Mockito.any(StringBuilder.class));
+
+ final ServerH2IOEventHandler handler = new ServerH2IOEventHandler(multiplexer);
+ final String text;
+ try {
+ text = handler.toString();
+ } finally {
+ handler.close();
+ }
+
+ Assertions.assertTrue(text.contains("state"));
+ Assertions.assertTrue(text.contains("->"));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2PrefaceHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2PrefaceHandler.java
new file mode 100644
index 0000000000..875d72f984
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2PrefaceHandler.java
@@ -0,0 +1,64 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerH2PrefaceHandler {
+
+ @Test
+ void rejectsInvalidPreface() throws Exception {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ServerH2StreamMultiplexerFactory factory = new ServerH2StreamMultiplexerFactory(
+ httpProcessor, exchangeHandlerFactory, null, null, null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final ServerH2PrefaceHandler handler = new ServerH2PrefaceHandler(ioSession, factory, null);
+
+ final byte[] invalid = ServerH2PrefaceHandler.PREFACE.clone();
+ invalid[0] = (byte) (invalid[0] + 1);
+
+ try {
+ Assertions.assertThrows(ProtocolNegotiationException.class, () ->
+ handler.inputReady(ioSession, ByteBuffer.wrap(invalid)));
+ } finally {
+ handler.close();
+ }
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java
new file mode 100644
index 0000000000..cf3f7c5a1a
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamHandler.java
@@ -0,0 +1,84 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
+import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerH2StreamHandler {
+
+ private ServerH2StreamHandler newHandler() {
+ final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics(
+ new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics());
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ServerH2StreamHandler(channel, httpProcessor, metrics, exchangeHandlerFactory,
+ HttpCoreContext.create());
+ }
+
+ @Test
+ void defaults() {
+ final ServerH2StreamHandler handler = newHandler();
+ Assertions.assertNull(handler.getPushHandlerFactory());
+ Assertions.assertFalse(handler.isOutputReady());
+ }
+
+ @Test
+ void consumeDataBeforeHeadersRejected() {
+ final ServerH2StreamHandler handler = newHandler();
+ Assertions.assertThrows(ProtocolException.class, () ->
+ handler.consumeData(ByteBuffer.allocate(0), true));
+ }
+
+ @Test
+ void updateCapacityWithoutHandlerFails() {
+ final ServerH2StreamHandler handler = newHandler();
+ Assertions.assertThrows(IllegalStateException.class, () -> handler.updateInputCapacity());
+ }
+
+ @Test
+ void toStringIncludesStates() {
+ final ServerH2StreamHandler handler = newHandler();
+ final String text = handler.toString();
+ Assertions.assertTrue(text.contains("requestState"));
+ Assertions.assertTrue(text.contains("responseState"));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamMultiplexer.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamMultiplexer.java
new file mode 100644
index 0000000000..54c5319d3a
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamMultiplexer.java
@@ -0,0 +1,115 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.AsyncPushProducer;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.H2ConnectionException;
+import org.apache.hc.core5.http2.H2Error;
+import org.apache.hc.core5.http2.config.H2Config;
+import org.apache.hc.core5.http2.frame.DefaultFrameFactory;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerH2StreamMultiplexer {
+
+ private ServerH2StreamMultiplexer newMultiplexer() {
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ServerH2StreamMultiplexer(
+ ioSession,
+ DefaultFrameFactory.INSTANCE,
+ httpProcessor,
+ exchangeHandlerFactory,
+ null,
+ H2Config.DEFAULT,
+ null);
+ }
+
+ @Test
+ void acceptPushFrameRejected() {
+ final ServerH2StreamMultiplexer multiplexer = newMultiplexer();
+ final H2ConnectionException ex = Assertions.assertThrows(H2ConnectionException.class, multiplexer::acceptPushFrame);
+ Assertions.assertEquals(H2Error.PROTOCOL_ERROR.getCode(), ex.getCode());
+ }
+
+ @Test
+ void outgoingRequestRejected() {
+ final ServerH2StreamMultiplexer multiplexer = newMultiplexer();
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ Assertions.assertThrows(H2ConnectionException.class, () ->
+ multiplexer.outgoingRequest(Mockito.mock(H2StreamChannel.class),
+ Mockito.mock(AsyncClientExchangeHandler.class),
+ pushHandlerFactory,
+ null));
+ }
+
+ @Test
+ void incomingPushPromiseRejected() {
+ final ServerH2StreamMultiplexer multiplexer = newMultiplexer();
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ Assertions.assertThrows(H2ConnectionException.class, () ->
+ multiplexer.incomingPushPromise(Mockito.mock(H2StreamChannel.class), pushHandlerFactory));
+ }
+
+ @Test
+ void outgoingPushPromiseCreatesHandler() throws Exception {
+ final ServerH2StreamMultiplexer multiplexer = newMultiplexer();
+ final H2StreamHandler handler = multiplexer.outgoingPushPromise(
+ Mockito.mock(H2StreamChannel.class),
+ Mockito.mock(AsyncPushProducer.class));
+ Assertions.assertNotNull(handler);
+ }
+
+ @Test
+ void allowGracefulAbortIsFalse() {
+ final ServerH2StreamMultiplexer multiplexer = newMultiplexer();
+ Assertions.assertFalse(multiplexer.allowGracefulAbort(Mockito.mock(H2Stream.class)));
+ }
+
+ @Test
+ void toStringContainsState() {
+ final ServerH2StreamMultiplexer multiplexer = newMultiplexer();
+ final String text = multiplexer.toString();
+ Assertions.assertTrue(text.startsWith("["));
+ Assertions.assertTrue(text.endsWith("]"));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamMultiplexerFactory.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamMultiplexerFactory.java
new file mode 100644
index 0000000000..971b2d1fd5
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2StreamMultiplexerFactory.java
@@ -0,0 +1,54 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerH2StreamMultiplexerFactory {
+
+ @Test
+ void createUsesDefaults() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ServerH2StreamMultiplexerFactory factory = new ServerH2StreamMultiplexerFactory(
+ httpProcessor, exchangeHandlerFactory, null, null, null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final ServerH2StreamMultiplexer multiplexer = factory.create(ioSession);
+
+ Assertions.assertNotNull(multiplexer);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2UpgradeHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2UpgradeHandler.java
new file mode 100644
index 0000000000..544fbce6b1
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerH2UpgradeHandler.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerH2UpgradeHandler {
+
+ @Test
+ void upgradeRegistersPrefaceHandler() throws Exception {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ServerH2StreamMultiplexerFactory factory = new ServerH2StreamMultiplexerFactory(
+ httpProcessor, exchangeHandlerFactory, null, null, null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ Mockito.when(ioSession.getLock()).thenReturn(new ReentrantLock());
+
+ final ServerH2UpgradeHandler handler = new ServerH2UpgradeHandler(factory, (Callback) ex -> {
+ });
+ handler.upgrade(ioSession, null);
+
+ Mockito.verify(ioSession).upgrade(Mockito.any());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerHttp1UpgradeHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerHttp1UpgradeHandler.java
new file mode 100644
index 0000000000..bfdff3a0fa
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerHttp1UpgradeHandler.java
@@ -0,0 +1,89 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import javax.net.ssl.SSLSession;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.config.CharCodingConfig;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.impl.nio.ServerHttp1StreamDuplexerFactory;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.apache.hc.core5.reactor.ssl.TlsDetails;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerHttp1UpgradeHandler {
+
+ @Test
+ void upgradeCreatesHandlerAndCompletesCallback() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ final ServerHttp1StreamDuplexerFactory factory = new ServerHttp1StreamDuplexerFactory(
+ httpProcessor,
+ exchangeHandlerFactory,
+ Http1Config.DEFAULT,
+ CharCodingConfig.DEFAULT,
+ null,
+ null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ Mockito.when(ioSession.poll()).thenReturn(null);
+ final SSLSession sslSession = Mockito.mock(SSLSession.class);
+ Mockito.when(ioSession.getTlsDetails()).thenReturn(new TlsDetails(sslSession, "h2"));
+
+ final ServerHttp1UpgradeHandler handler = new ServerHttp1UpgradeHandler(factory);
+ final AtomicReference completed = new AtomicReference<>();
+
+ handler.upgrade(ioSession, new FutureCallback() {
+ @Override
+ public void completed(final ProtocolIOSession result) {
+ completed.set(result);
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ }
+
+ @Override
+ public void cancelled() {
+ }
+ });
+
+ Assertions.assertSame(ioSession, completed.get());
+ Mockito.verify(ioSession, Mockito.times(2)).upgrade(Mockito.any());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerHttpProtocolNegotiationStarter.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerHttpProtocolNegotiationStarter.java
new file mode 100644
index 0000000000..060dd66fc3
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerHttpProtocolNegotiationStarter.java
@@ -0,0 +1,128 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.impl.nio.ServerHttp1IOEventHandler;
+import org.apache.hc.core5.http.impl.nio.ServerHttp1StreamDuplexerFactory;
+import org.apache.hc.core5.http.config.CharCodingConfig;
+import org.apache.hc.core5.http.config.Http1Config;
+import org.apache.hc.core5.http.nio.AsyncServerExchangeHandler;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.http2.ssl.ApplicationProtocol;
+import org.apache.hc.core5.reactor.EndpointParameters;
+import org.apache.hc.core5.reactor.ProtocolIOSession;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerHttpProtocolNegotiationStarter {
+
+ private static ServerHttp1StreamDuplexerFactory http1Factory() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ServerHttp1StreamDuplexerFactory(
+ httpProcessor,
+ exchangeHandlerFactory,
+ Http1Config.DEFAULT,
+ CharCodingConfig.DEFAULT,
+ null,
+ null);
+ }
+
+ private static ServerH2StreamMultiplexerFactory http2Factory() {
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory exchangeHandlerFactory =
+ (HandlerFactory) Mockito.mock(HandlerFactory.class);
+ return new ServerH2StreamMultiplexerFactory(httpProcessor, exchangeHandlerFactory, null, null, null);
+ }
+
+ @Test
+ void forceHttp2UsesPrefaceHandlerAndTlsUpgrade() {
+ final TlsStrategy tlsStrategy = Mockito.mock(TlsStrategy.class);
+ final ServerHttpProtocolNegotiationStarter starter = new ServerHttpProtocolNegotiationStarter(
+ http1Factory(),
+ http2Factory(),
+ HttpVersionPolicy.FORCE_HTTP_2,
+ tlsStrategy,
+ Timeout.ofSeconds(1),
+ (Callback) ex -> {
+ });
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final EndpointParameters endpoint = new EndpointParameters(URIScheme.HTTPS.id, "localhost", 8443,
+ HttpVersionPolicy.FORCE_HTTP_2);
+
+ final Object handler = starter.createHandler(ioSession, endpoint);
+
+ Assertions.assertTrue(handler instanceof ServerH2PrefaceHandler);
+ Mockito.verify(ioSession).registerProtocol(Mockito.eq(ApplicationProtocol.HTTP_1_1.id), Mockito.any());
+ Mockito.verify(ioSession).registerProtocol(Mockito.eq(ApplicationProtocol.HTTP_2.id), Mockito.any());
+ Mockito.verify(tlsStrategy).upgrade(ioSession, endpoint, HttpVersionPolicy.FORCE_HTTP_2, Timeout.ofSeconds(1), null);
+ }
+
+ @Test
+ void forceHttp1UsesHttp1Handler() {
+ final ServerHttpProtocolNegotiationStarter starter = new ServerHttpProtocolNegotiationStarter(
+ http1Factory(),
+ http2Factory(),
+ HttpVersionPolicy.FORCE_HTTP_1,
+ null,
+ null,
+ null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final Object handler = starter.createHandler(ioSession, null);
+
+ Assertions.assertTrue(handler instanceof ServerHttp1IOEventHandler);
+ }
+
+ @Test
+ void negotiateUsesProtocolNegotiator() {
+ final ServerHttpProtocolNegotiationStarter starter = new ServerHttpProtocolNegotiationStarter(
+ http1Factory(),
+ http2Factory(),
+ HttpVersionPolicy.NEGOTIATE,
+ null,
+ null,
+ null);
+
+ final ProtocolIOSession ioSession = Mockito.mock(ProtocolIOSession.class);
+ final Object handler = starter.createHandler(ioSession, null);
+
+ Assertions.assertTrue(handler instanceof HttpProtocolNegotiator);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerPushH2StreamHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerPushH2StreamHandler.java
new file mode 100644
index 0000000000..52ff2c8df9
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/impl/nio/TestServerPushH2StreamHandler.java
@@ -0,0 +1,68 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.impl.nio;
+
+import java.nio.ByteBuffer;
+
+import org.apache.hc.core5.http.ProtocolException;
+import org.apache.hc.core5.http.impl.BasicHttpConnectionMetrics;
+import org.apache.hc.core5.http.impl.BasicHttpTransportMetrics;
+import org.apache.hc.core5.http.nio.AsyncPushProducer;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.protocol.HttpProcessor;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestServerPushH2StreamHandler {
+
+ private ServerPushH2StreamHandler newHandler() {
+ final H2StreamChannel channel = Mockito.mock(H2StreamChannel.class);
+ final HttpProcessor httpProcessor = Mockito.mock(HttpProcessor.class);
+ final BasicHttpConnectionMetrics metrics = new BasicHttpConnectionMetrics(
+ new BasicHttpTransportMetrics(), new BasicHttpTransportMetrics());
+ final AsyncPushProducer pushProducer = Mockito.mock(AsyncPushProducer.class);
+ Mockito.when(pushProducer.available()).thenReturn(0);
+ return new ServerPushH2StreamHandler(channel, httpProcessor, metrics, pushProducer, HttpCoreContext.create());
+ }
+
+ @Test
+ void defaults() {
+ final ServerPushH2StreamHandler handler = newHandler();
+ Assertions.assertNull(handler.getPushHandlerFactory());
+ Assertions.assertTrue(handler.isOutputReady());
+ }
+
+ @Test
+ void consumesAreRejected() {
+ final ServerPushH2StreamHandler handler = newHandler();
+ Assertions.assertThrows(ProtocolException.class, () -> handler.consumePromise(null));
+ Assertions.assertThrows(ProtocolException.class, () -> handler.consumeHeader(null, true));
+ Assertions.assertThrows(ProtocolException.class, () -> handler.consumeData(ByteBuffer.allocate(0), true));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/command/TestPingCommand.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/command/TestPingCommand.java
new file mode 100644
index 0000000000..84833f40f1
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/command/TestPingCommand.java
@@ -0,0 +1,47 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.nio.command;
+
+import org.apache.hc.core5.http2.nio.AsyncPingHandler;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestPingCommand {
+
+ @Test
+ void testCancelDelegatesToHandler() {
+ final AsyncPingHandler handler = Mockito.mock(AsyncPingHandler.class);
+ final PingCommand command = new PingCommand(handler);
+
+ Assertions.assertSame(handler, command.getHandler());
+ Assertions.assertTrue(command.cancel());
+
+ Mockito.verify(handler).cancel();
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/command/TestPushResponseCommand.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/command/TestPushResponseCommand.java
new file mode 100644
index 0000000000..8c342454db
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/command/TestPushResponseCommand.java
@@ -0,0 +1,49 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.nio.command;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestPushResponseCommand {
+
+ @Test
+ void testRejectsNonPositiveStreamId() {
+ Assertions.assertThrows(IllegalArgumentException.class, () -> new PushResponseCommand(0));
+ }
+
+ @Test
+ void testCancelTogglesState() {
+ final PushResponseCommand command = new PushResponseCommand(1);
+
+ Assertions.assertFalse(command.isCancelled());
+ Assertions.assertTrue(command.cancel());
+ Assertions.assertTrue(command.isCancelled());
+ Assertions.assertFalse(command.cancel());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/pool/TestH2ConnPool.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/pool/TestH2ConnPool.java
new file mode 100644
index 0000000000..b9bf2558c4
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/pool/TestH2ConnPool.java
@@ -0,0 +1,227 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.nio.pool;
+
+import java.net.InetSocketAddress;
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.function.Resolver;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http.nio.command.ShutdownCommand;
+import org.apache.hc.core5.http.nio.command.StaleCheckCommand;
+import org.apache.hc.core5.http.nio.ssl.TlsStrategy;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.Command;
+import org.apache.hc.core5.reactor.ConnectionInitiator;
+import org.apache.hc.core5.reactor.IOSession;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestH2ConnPool {
+
+ @Test
+ void testCloseSessionGracefulEnqueuesShutdown() {
+ final ConnectionInitiator connectionInitiator = Mockito.mock(ConnectionInitiator.class);
+ try (H2ConnPool pool = new H2ConnPool(connectionInitiator, null, null)) {
+ final IOSession session = Mockito.mock(IOSession.class);
+
+ pool.closeSession(session, CloseMode.GRACEFUL);
+
+ Mockito.verify(session).enqueue(ShutdownCommand.GRACEFUL, Command.Priority.NORMAL);
+ }
+ }
+
+ @Test
+ void testCloseSessionImmediateCloses() {
+ final ConnectionInitiator connectionInitiator = Mockito.mock(ConnectionInitiator.class);
+ try (H2ConnPool pool = new H2ConnPool(connectionInitiator, null, null)) {
+ final IOSession session = Mockito.mock(IOSession.class);
+
+ pool.closeSession(session, CloseMode.IMMEDIATE);
+
+ Mockito.verify(session).close(CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void testValidateSessionClosed() {
+ final ConnectionInitiator connectionInitiator = Mockito.mock(ConnectionInitiator.class);
+ try (H2ConnPool pool = new H2ConnPool(connectionInitiator, null, null)) {
+ final IOSession session = Mockito.mock(IOSession.class);
+ Mockito.when(session.isOpen()).thenReturn(false);
+
+ final AtomicReference result = new AtomicReference<>();
+ pool.validateSession(session, result::set);
+
+ Assertions.assertEquals(Boolean.FALSE, result.get());
+ }
+ }
+
+ @Test
+ void testValidateSessionEnqueuesStaleCheck() {
+ final ConnectionInitiator connectionInitiator = Mockito.mock(ConnectionInitiator.class);
+ try (H2ConnPool pool = new H2ConnPool(connectionInitiator, null, null)) {
+ pool.setValidateAfterInactivity(TimeValue.ZERO_MILLISECONDS);
+
+ final IOSession session = Mockito.mock(IOSession.class);
+ Mockito.when(session.isOpen()).thenReturn(true);
+ Mockito.when(session.getLastReadTime()).thenReturn(0L);
+ Mockito.when(session.getLastWriteTime()).thenReturn(0L);
+
+ @SuppressWarnings("unchecked")
+ final Callback callback = (Callback) Mockito.mock(Callback.class);
+ pool.validateSession(session, callback);
+
+ Mockito.verify(session).enqueue(Mockito.any(StaleCheckCommand.class), Mockito.eq(Command.Priority.NORMAL));
+ Mockito.verifyNoInteractions(callback);
+ }
+ }
+
+ @Test
+ void testConnectSessionPlain() {
+ final ConnectionInitiator connectionInitiator = Mockito.mock(ConnectionInitiator.class);
+ final Resolver resolver = endpoint ->
+ new InetSocketAddress("localhost", endpoint.getPort());
+ try (H2ConnPool pool = new H2ConnPool(connectionInitiator, resolver, null)) {
+ final HttpHost host = new HttpHost(URIScheme.HTTP.id, "localhost", 7443);
+ final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", 7443);
+ final Timeout connectTimeout = Timeout.ofSeconds(1);
+ final IOSession session = Mockito.mock(IOSession.class);
+
+ Mockito.when(connectionInitiator.connect(
+ Mockito.eq(host),
+ Mockito.eq(remoteAddress),
+ Mockito.isNull(),
+ Mockito.eq(connectTimeout),
+ Mockito.isNull(),
+ Mockito.any()))
+ .thenAnswer(invocation -> {
+ final FutureCallback cb = invocation.getArgument(5);
+ cb.completed(session);
+ return CompletableFuture.completedFuture(session);
+ });
+
+ final AtomicReference result = new AtomicReference<>();
+ pool.connectSession(host, connectTimeout, new FutureCallback() {
+
+ @Override
+ public void completed(final IOSession ioSession) {
+ result.set(ioSession);
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ }
+
+ @Override
+ public void cancelled() {
+ }
+
+ });
+
+ Assertions.assertSame(session, result.get());
+ }
+ }
+
+ @Test
+ void testConnectSessionUpgradesTls() {
+ final ConnectionInitiator connectionInitiator = Mockito.mock(ConnectionInitiator.class);
+ final Resolver resolver = endpoint ->
+ new InetSocketAddress("localhost", endpoint.getPort());
+ final TlsStrategy tlsStrategy = Mockito.mock(TlsStrategy.class);
+ try (H2ConnPool pool = new H2ConnPool(connectionInitiator, resolver, tlsStrategy)) {
+ final HttpHost host = new HttpHost(URIScheme.HTTPS.id, "localhost", 8443);
+ final InetSocketAddress remoteAddress = new InetSocketAddress("localhost", 8443);
+ final Timeout connectTimeout = Timeout.ofSeconds(1);
+ final IOSession session = Mockito.mock(IOSession.class, Mockito.withSettings()
+ .extraInterfaces(TransportSecurityLayer.class));
+ final TransportSecurityLayer tlsLayer = (TransportSecurityLayer) session;
+
+ Mockito.when(connectionInitiator.connect(
+ Mockito.eq(host),
+ Mockito.eq(remoteAddress),
+ Mockito.isNull(),
+ Mockito.eq(connectTimeout),
+ Mockito.isNull(),
+ Mockito.any()))
+ .thenAnswer(invocation -> {
+ final FutureCallback cb = invocation.getArgument(5);
+ cb.completed(session);
+ return CompletableFuture.completedFuture(session);
+ });
+
+ Mockito.doAnswer(invocation -> {
+ final FutureCallback cb = invocation.getArgument(4);
+ if (cb != null) {
+ cb.completed(tlsLayer);
+ }
+ return null;
+ }).when(tlsStrategy).upgrade(
+ Mockito.eq(tlsLayer),
+ Mockito.eq(host),
+ Mockito.isNull(),
+ Mockito.eq(connectTimeout),
+ Mockito.any());
+
+ final AtomicReference result = new AtomicReference<>();
+ pool.connectSession(host, connectTimeout, new FutureCallback() {
+
+ @Override
+ public void completed(final IOSession ioSession) {
+ result.set(ioSession);
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ }
+
+ @Override
+ public void cancelled() {
+ }
+
+ });
+
+ Assertions.assertSame(session, result.get());
+ Mockito.verify(tlsStrategy).upgrade(
+ Mockito.eq(tlsLayer),
+ Mockito.eq(host),
+ Mockito.isNull(),
+ Mockito.eq(connectTimeout),
+ Mockito.any());
+ Mockito.verify(session).setSocketTimeout(connectTimeout);
+ }
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/support/TestBasicPingHandler.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/support/TestBasicPingHandler.java
new file mode 100644
index 0000000000..fddfdd0e29
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/support/TestBasicPingHandler.java
@@ -0,0 +1,71 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.nio.support;
+
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestBasicPingHandler {
+
+ @Test
+ void testConsumeResponseMatches() throws Exception {
+ final AtomicReference result = new AtomicReference<>();
+ final BasicPingHandler handler = new BasicPingHandler(result::set);
+
+ handler.consumeResponse(handler.getData());
+
+ Assertions.assertEquals(Boolean.TRUE, result.get());
+ }
+
+ @Test
+ void testConsumeResponseMismatch() throws Exception {
+ final AtomicReference result = new AtomicReference<>();
+ final BasicPingHandler handler = new BasicPingHandler(result::set);
+
+ handler.consumeResponse(ByteBuffer.wrap(new byte[] {'x'}));
+
+ Assertions.assertEquals(Boolean.FALSE, result.get());
+ }
+
+ @Test
+ void testFailedAndCancel() {
+ final AtomicReference result = new AtomicReference<>(Boolean.TRUE);
+ final BasicPingHandler handler = new BasicPingHandler(result::set);
+
+ handler.failed(new IOException("boom"));
+ Assertions.assertEquals(Boolean.FALSE, result.get());
+
+ result.set(Boolean.TRUE);
+ handler.cancel();
+ Assertions.assertEquals(Boolean.FALSE, result.get());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/support/TestDefaultAsyncPushConsumerFactory.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/support/TestDefaultAsyncPushConsumerFactory.java
new file mode 100644
index 0000000000..861b4e5aa7
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/nio/support/TestDefaultAsyncPushConsumerFactory.java
@@ -0,0 +1,90 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.nio.support;
+
+import org.apache.hc.core5.function.Supplier;
+import org.apache.hc.core5.http.MisdirectedRequestException;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.http.HttpRequestMapper;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestDefaultAsyncPushConsumerFactory {
+
+ @Test
+ void testCreateResolvesConsumer() throws Exception {
+ final HttpRequestMapper> mapper = mockMapper();
+ final AsyncPushConsumer consumer = Mockito.mock(AsyncPushConsumer.class);
+ Mockito.when(mapper.resolve(Mockito.any(), Mockito.any())).thenReturn(() -> consumer);
+
+ final DefaultAsyncPushConsumerFactory factory = new DefaultAsyncPushConsumerFactory(mapper);
+
+ final AsyncPushConsumer result = factory.create(
+ new BasicHttpRequest("GET", "/"),
+ HttpCoreContext.create());
+
+ Assertions.assertSame(consumer, result);
+ }
+
+ @Test
+ void testCreateReturnsNullWhenNotResolved() throws Exception {
+ final HttpRequestMapper> mapper = mockMapper();
+ Mockito.when(mapper.resolve(Mockito.any(), Mockito.any())).thenReturn(null);
+
+ final DefaultAsyncPushConsumerFactory factory = new DefaultAsyncPushConsumerFactory(mapper);
+
+ final AsyncPushConsumer result = factory.create(
+ new BasicHttpRequest("GET", "/"),
+ HttpCoreContext.create());
+
+ Assertions.assertNull(result);
+ }
+
+ @Test
+ void testCreateHandlesMisdirectedRequest() throws Exception {
+ final HttpRequestMapper> mapper = mockMapper();
+ Mockito.when(mapper.resolve(Mockito.any(), Mockito.any()))
+ .thenThrow(new MisdirectedRequestException("wrong authority"));
+
+ final DefaultAsyncPushConsumerFactory factory = new DefaultAsyncPushConsumerFactory(mapper);
+
+ final AsyncPushConsumer result = factory.create(
+ new BasicHttpRequest("GET", "/"),
+ HttpCoreContext.create());
+
+ Assertions.assertNull(result);
+ }
+
+ @SuppressWarnings("unchecked")
+ private static HttpRequestMapper> mockMapper() {
+ return (HttpRequestMapper>) Mockito.mock(HttpRequestMapper.class);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConnControl.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConnControl.java
new file mode 100644
index 0000000000..0d6b15d99f
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestConnControl.java
@@ -0,0 +1,60 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.protocol;
+
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestH2RequestConnControl {
+
+ @Test
+ void addsConnectionHeaderForHttp1() throws Exception {
+ final BasicHttpRequest request = new BasicHttpRequest("GET", "/");
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ H2RequestConnControl.INSTANCE.process(request, null, context);
+
+ Assertions.assertTrue(request.containsHeader(HttpHeaders.CONNECTION));
+ }
+
+ @Test
+ void skipsConnectionHeaderForHttp2() throws Exception {
+ final BasicHttpRequest request = new BasicHttpRequest("GET", "/");
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_2);
+
+ H2RequestConnControl.INSTANCE.process(request, null, context);
+
+ Assertions.assertFalse(request.containsHeader(HttpHeaders.CONNECTION));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTargetHost.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTargetHost.java
new file mode 100644
index 0000000000..e66cf5420c
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestTargetHost.java
@@ -0,0 +1,63 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.protocol;
+
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.apache.hc.core5.net.URIAuthority;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestH2RequestTargetHost {
+
+ @Test
+ void addsHostHeaderForHttp1() throws Exception {
+ final BasicHttpRequest request = new BasicHttpRequest("GET", "/");
+ request.setAuthority(new URIAuthority("example.com"));
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ H2RequestTargetHost.INSTANCE.process(request, null, context);
+
+ Assertions.assertTrue(request.containsHeader(HttpHeaders.HOST));
+ }
+
+ @Test
+ void skipsHostHeaderForHttp2() throws Exception {
+ final BasicHttpRequest request = new BasicHttpRequest("GET", "/");
+ request.setAuthority(new URIAuthority("example.com"));
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_2);
+
+ H2RequestTargetHost.INSTANCE.process(request, null, context);
+
+ Assertions.assertFalse(request.containsHeader(HttpHeaders.HOST));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java
new file mode 100644
index 0000000000..cb4ec9cb43
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2RequestValidateHost.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.protocol;
+
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestH2RequestValidateHost {
+
+ @Test
+ void validatesHostForHttp1() throws Exception {
+ final BasicHttpRequest request = new BasicHttpRequest("GET", "/");
+ request.addHeader(HttpHeaders.HOST, "example.com");
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ H2RequestValidateHost.INSTANCE.process(request, null, context);
+
+ Assertions.assertNotNull(request.getAuthority());
+ }
+
+ @Test
+ void skipsValidationForHttp2() throws Exception {
+ final BasicHttpRequest request = new BasicHttpRequest("GET", "/");
+ request.addHeader(HttpHeaders.HOST, "example.com");
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_2);
+
+ H2RequestValidateHost.INSTANCE.process(request, null, context);
+
+ Assertions.assertNull(request.getAuthority());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2ResponseConnControl.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2ResponseConnControl.java
new file mode 100644
index 0000000000..7b3af4dccd
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2ResponseConnControl.java
@@ -0,0 +1,62 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.protocol;
+
+import org.apache.hc.core5.http.HeaderElements;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpStatus;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestH2ResponseConnControl {
+
+ @Test
+ void addsConnectionHeaderForHttp1OnError() throws Exception {
+ final BasicHttpResponse response = new BasicHttpResponse(HttpStatus.SC_BAD_REQUEST);
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ H2ResponseConnControl.INSTANCE.process(response, null, context);
+
+ Assertions.assertEquals(HeaderElements.CLOSE, response.getFirstHeader(HttpHeaders.CONNECTION).getValue());
+ }
+
+ @Test
+ void skipsConnectionHeaderForHttp2() throws Exception {
+ final BasicHttpResponse response = new BasicHttpResponse(HttpStatus.SC_BAD_REQUEST);
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_2);
+
+ H2ResponseConnControl.INSTANCE.process(response, null, context);
+
+ Assertions.assertFalse(response.containsHeader(HttpHeaders.CONNECTION));
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2ResponseContent.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2ResponseContent.java
new file mode 100644
index 0000000000..45a19f9f33
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/protocol/TestH2ResponseContent.java
@@ -0,0 +1,65 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.protocol;
+
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHeaders;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.impl.BasicEntityDetails;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestH2ResponseContent {
+
+ @Test
+ void addsContentHeadersForHttp2Entity() throws Exception {
+ final BasicHttpResponse response = new BasicHttpResponse(200);
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_2);
+
+ final BasicEntityDetails entity = new BasicEntityDetails(10, ContentType.TEXT_PLAIN);
+ H2ResponseContent.INSTANCE.process(response, entity, context);
+
+ Assertions.assertTrue(response.containsHeader(HttpHeaders.CONTENT_TYPE));
+ Assertions.assertFalse(response.containsHeader(HttpHeaders.CONTENT_LENGTH));
+ }
+
+ @Test
+ void addsContentLengthForHttp1() throws Exception {
+ final BasicHttpResponse response = new BasicHttpResponse(200);
+ final HttpCoreContext context = HttpCoreContext.create();
+ context.setProtocolVersion(HttpVersion.HTTP_1_1);
+
+ final BasicEntityDetails entity = new BasicEntityDetails(5, ContentType.TEXT_PLAIN);
+ H2ResponseContent.INSTANCE.process(response, entity, context);
+
+ Assertions.assertEquals("5", response.getFirstHeader(HttpHeaders.CONTENT_LENGTH).getValue());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestApplicationProtocol.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestApplicationProtocol.java
new file mode 100644
index 0000000000..5300c7be99
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestApplicationProtocol.java
@@ -0,0 +1,40 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.ssl;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestApplicationProtocol {
+
+ @Test
+ void toStringReturnsProtocolId() {
+ Assertions.assertEquals("h2", ApplicationProtocol.HTTP_2.toString());
+ Assertions.assertEquals("http/1.1", ApplicationProtocol.HTTP_1_1.toString());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2ClientTlsStrategy.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2ClientTlsStrategy.java
new file mode 100644
index 0000000000..a06805de48
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2ClientTlsStrategy.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.ssl;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
+import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer;
+import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+class TestH2ClientTlsStrategy {
+
+ @Test
+ void upgradeConfiguresSslParameters() throws Exception {
+ final SSLContext sslContext = SSLContexts.createDefault();
+ final SSLSessionVerifier verifier = Mockito.mock(SSLSessionVerifier.class);
+ final H2ClientTlsStrategy strategy = new H2ClientTlsStrategy(sslContext, SSLBufferMode.STATIC, null, verifier);
+ final TransportSecurityLayer tlsSession = Mockito.mock(TransportSecurityLayer.class);
+ final HttpHost endpoint = new HttpHost(URIScheme.HTTPS.id, "example.com", 443);
+
+ final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(SSLSessionInitializer.class);
+
+ strategy.upgrade(tlsSession, endpoint, HttpVersionPolicy.FORCE_HTTP_2, Timeout.ofSeconds(1), null);
+
+ Mockito.verify(tlsSession).startTls(
+ Mockito.eq(sslContext),
+ Mockito.eq(endpoint),
+ Mockito.eq(SSLBufferMode.STATIC),
+ initializerCaptor.capture(),
+ Mockito.eq(verifier),
+ Mockito.eq(Timeout.ofSeconds(1)),
+ Mockito.>isNull());
+
+ final SSLSessionInitializer initializer = initializerCaptor.getValue();
+ final javax.net.ssl.SSLEngine sslEngine = sslContext.createSSLEngine();
+ initializer.initialize(endpoint, sslEngine);
+
+ final SSLParameters sslParameters = sslEngine.getSSLParameters();
+ Assertions.assertEquals(URIScheme.HTTPS.id, sslParameters.getEndpointIdentificationAlgorithm());
+ Assertions.assertArrayEquals(new String[] { ApplicationProtocol.HTTP_2.id },
+ sslParameters.getApplicationProtocols());
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void deprecatedUpgradeReturnsFalseForNonHttps() {
+ final SSLContext sslContext = SSLContexts.createDefault();
+ final H2ClientTlsStrategy strategy = new H2ClientTlsStrategy(sslContext);
+ final TransportSecurityLayer tlsSession = Mockito.mock(TransportSecurityLayer.class);
+ final HttpHost host = new HttpHost(URIScheme.HTTP.id, "localhost", 8080);
+
+ final boolean upgraded = strategy.upgrade(tlsSession, host, null, null, null, Timeout.ofSeconds(1));
+
+ Assertions.assertFalse(upgraded);
+ Mockito.verifyNoInteractions(tlsSession);
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2ServerTlsStrategy.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2ServerTlsStrategy.java
new file mode 100644
index 0000000000..f0e3900b13
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2ServerTlsStrategy.java
@@ -0,0 +1,119 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.ssl;
+
+import java.net.InetSocketAddress;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.URIScheme;
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.reactor.ssl.SSLBufferMode;
+import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer;
+import org.apache.hc.core5.reactor.ssl.SSLSessionVerifier;
+import org.apache.hc.core5.reactor.ssl.TransportSecurityLayer;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.ArgumentCaptor;
+import org.mockito.Mockito;
+
+@SuppressWarnings("deprecation")
+class TestH2ServerTlsStrategy {
+
+ @Test
+ void upgradeConfiguresSslParameters() throws Exception {
+ final SSLContext sslContext = SSLContexts.createDefault();
+ final SSLSessionVerifier verifier = Mockito.mock(SSLSessionVerifier.class);
+ final H2ServerTlsStrategy strategy = new H2ServerTlsStrategy(sslContext, SSLBufferMode.STATIC, null, verifier);
+ final TransportSecurityLayer tlsSession = Mockito.mock(TransportSecurityLayer.class);
+ final HttpHost endpoint = new HttpHost(URIScheme.HTTPS.id, "example.com", 443);
+
+ final ArgumentCaptor initializerCaptor = ArgumentCaptor.forClass(SSLSessionInitializer.class);
+
+ strategy.upgrade(tlsSession, endpoint, HttpVersionPolicy.FORCE_HTTP_1, Timeout.ofSeconds(1), null);
+
+ Mockito.verify(tlsSession).startTls(
+ Mockito.eq(sslContext),
+ Mockito.eq(endpoint),
+ Mockito.eq(SSLBufferMode.STATIC),
+ initializerCaptor.capture(),
+ Mockito.eq(verifier),
+ Mockito.eq(Timeout.ofSeconds(1)),
+ Mockito.>isNull());
+
+ final SSLSessionInitializer initializer = initializerCaptor.getValue();
+ final javax.net.ssl.SSLEngine sslEngine = sslContext.createSSLEngine();
+ initializer.initialize(endpoint, sslEngine);
+
+ final SSLParameters sslParameters = sslEngine.getSSLParameters();
+ Assertions.assertArrayEquals(new String[] { ApplicationProtocol.HTTP_1_1.id },
+ sslParameters.getApplicationProtocols());
+ }
+
+ @Test
+ void deprecatedUpgradeRespectsSecurePorts() {
+ final SSLContext sslContext = SSLContexts.createDefault();
+ final H2ServerTlsStrategy strategy = new H2ServerTlsStrategy(
+ sslContext,
+ new org.apache.hc.core5.http.nio.ssl.FixedPortStrategy(8443));
+ final TransportSecurityLayer tlsSession = Mockito.mock(TransportSecurityLayer.class);
+ final HttpHost host = new HttpHost(URIScheme.HTTPS.id, "localhost", 8443);
+
+ final boolean notUpgraded = strategy.upgrade(
+ tlsSession,
+ host,
+ new InetSocketAddress("localhost", 8080),
+ null,
+ null,
+ Timeout.ofSeconds(1));
+ Assertions.assertFalse(notUpgraded);
+ Mockito.verifyNoInteractions(tlsSession);
+
+ final boolean upgraded = strategy.upgrade(
+ tlsSession,
+ host,
+ new InetSocketAddress("localhost", 8443),
+ null,
+ null,
+ Timeout.ofSeconds(1));
+ Assertions.assertTrue(upgraded);
+ Mockito.verify(tlsSession).startTls(
+ Mockito.eq(sslContext),
+ Mockito.eq(host),
+ Mockito.isNull(),
+ Mockito.any(),
+ Mockito.isNull(),
+ Mockito.eq(Timeout.ofSeconds(1)),
+ Mockito.>isNull());
+ }
+
+}
diff --git a/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2TlsSupport.java b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2TlsSupport.java
new file mode 100644
index 0000000000..42717df0d1
--- /dev/null
+++ b/httpcore5-h2/src/test/java/org/apache/hc/core5/http2/ssl/TestH2TlsSupport.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http2.ssl;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLParameters;
+
+import org.apache.hc.core5.http2.HttpVersionPolicy;
+import org.apache.hc.core5.reactor.ssl.SSLSessionInitializer;
+import org.apache.hc.core5.ssl.SSLContexts;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestH2TlsSupport {
+
+ @Test
+ void selectApplicationProtocolsDefault() {
+ Assertions.assertArrayEquals(
+ new String[] { ApplicationProtocol.HTTP_2.id, ApplicationProtocol.HTTP_1_1.id },
+ H2TlsSupport.selectApplicationProtocols(null));
+ }
+
+ @Test
+ void selectApplicationProtocolsForced() {
+ Assertions.assertArrayEquals(
+ new String[] { ApplicationProtocol.HTTP_2.id },
+ H2TlsSupport.selectApplicationProtocols(HttpVersionPolicy.FORCE_HTTP_2));
+ Assertions.assertArrayEquals(
+ new String[] { ApplicationProtocol.HTTP_1_1.id },
+ H2TlsSupport.selectApplicationProtocols(HttpVersionPolicy.FORCE_HTTP_1));
+ }
+
+ @Test
+ void enforceRequirementsSetsApplicationProtocols() {
+ final SSLParameters sslParameters = new SSLParameters(
+ new String[] {"TLSv1.3", "TLSv1.2"},
+ new String[] {"TLS_AES_128_GCM_SHA256"});
+
+ H2TlsSupport.enforceRequirements(HttpVersionPolicy.FORCE_HTTP_2, sslParameters);
+
+ Assertions.assertArrayEquals(
+ new String[] { ApplicationProtocol.HTTP_2.id },
+ sslParameters.getApplicationProtocols());
+ }
+
+ @Test
+ void enforceRequirementsInitializerInvoked() throws Exception {
+ final SSLContext sslContext = SSLContexts.createDefault();
+ final SSLSessionInitializer[] called = new SSLSessionInitializer[1];
+ final SSLSessionInitializer[] holder = new SSLSessionInitializer[1];
+ holder[0] = (endpoint, sslEngine) -> called[0] = holder[0];
+ final SSLSessionInitializer initializer = holder[0];
+
+ final SSLSessionInitializer enforcing = H2TlsSupport.enforceRequirements(
+ HttpVersionPolicy.FORCE_HTTP_1, initializer);
+ enforcing.initialize(null, sslContext.createSSLEngine());
+
+ Assertions.assertSame(initializer, called[0]);
+ }
+
+}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java
index 73ff66f551..aecc6b83fb 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/classic/ClassicIntegrationTest.java
@@ -729,6 +729,108 @@ void testNoContentResponse() throws Exception {
}
}
+ @Test
+ void testEchoNoEntityRequest() throws Exception {
+ final ClassicTestServer server = testResources.server();
+ final ClassicTestClient client = testResources.client();
+
+ server.register("*", new EchoHandler());
+
+ server.start();
+ client.start();
+
+ final HttpCoreContext context = HttpCoreContext.create();
+ final HttpHost host = new HttpHost(scheme.id, "localhost", server.getPort());
+
+ final BasicClassicHttpRequest post = new BasicClassicHttpRequest(Method.POST, "/echo");
+ post.setEntity(null);
+
+ try (final ClassicHttpResponse response = client.execute(host, post, context)) {
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ if (response.getEntity() != null) {
+ final byte[] received = EntityUtils.toByteArray(response.getEntity());
+ Assertions.assertEquals(0, received.length);
+ }
+ }
+ }
+
+ @Test
+ void testEchoEmptyEntity() throws Exception {
+ final ClassicTestServer server = testResources.server();
+ final ClassicTestClient client = testResources.client();
+
+ server.register("*", new EchoHandler());
+
+ server.start();
+ client.start();
+
+ final HttpCoreContext context = HttpCoreContext.create();
+ final HttpHost host = new HttpHost(scheme.id, "localhost", server.getPort());
+
+ final BasicClassicHttpRequest post = new BasicClassicHttpRequest(Method.POST, "/echo");
+ post.setEntity(new ByteArrayEntity(new byte[0], ContentType.TEXT_PLAIN));
+
+ try (final ClassicHttpResponse response = client.execute(host, post, context)) {
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ final byte[] received = EntityUtils.toByteArray(response.getEntity());
+ Assertions.assertEquals(0, received.length);
+ }
+ }
+
+ @Test
+ void testEchoLargeEntity() throws Exception {
+ final ClassicTestServer server = testResources.server();
+ final ClassicTestClient client = testResources.client();
+
+ server.register("*", new EchoHandler());
+
+ server.start();
+ client.start();
+
+ final HttpCoreContext context = HttpCoreContext.create();
+ final HttpHost host = new HttpHost(scheme.id, "localhost", server.getPort());
+
+ final byte[] data = new byte[128 * 1024];
+ for (int i = 0; i < data.length; i++) {
+ data[i] = (byte) (i % 256);
+ }
+
+ final BasicClassicHttpRequest post = new BasicClassicHttpRequest(Method.POST, "/echo");
+ post.setEntity(new ByteArrayEntity(data, ContentType.APPLICATION_OCTET_STREAM));
+
+ try (final ClassicHttpResponse response = client.execute(host, post, context)) {
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ final byte[] received = EntityUtils.toByteArray(response.getEntity());
+ Assertions.assertArrayEquals(data, received);
+ }
+ }
+
+ @Test
+ void testEchoManySmallRequests() throws Exception {
+ final ClassicTestServer server = testResources.server();
+ final ClassicTestClient client = testResources.client();
+
+ server.register("*", new EchoHandler());
+
+ server.start();
+ client.start();
+
+ final HttpCoreContext context = HttpCoreContext.create();
+ final HttpHost host = new HttpHost(scheme.id, "localhost", server.getPort());
+
+ for (int i = 0; i < 10; i++) {
+ final byte[] data = ("hi-" + i).getBytes(StandardCharsets.US_ASCII);
+ final BasicClassicHttpRequest post = new BasicClassicHttpRequest(Method.POST, "/echo");
+ post.setEntity(new ByteArrayEntity(data, ContentType.TEXT_PLAIN));
+
+ try (final ClassicHttpResponse response = client.execute(host, post, context)) {
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ final byte[] received = EntityUtils.toByteArray(response.getEntity());
+ Assertions.assertArrayEquals(data, received);
+ }
+ }
+ }
+
@Test
void testHeaderTooLarge() throws Exception {
final ClassicTestServer server = testResources.server();
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClassicTestClientAdapter.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClassicTestClientAdapter.java
new file mode 100644
index 0000000000..c895f08c4f
--- /dev/null
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClassicTestClientAdapter.java
@@ -0,0 +1,186 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.testing.framework;
+
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.BODY;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.CONTENT_TYPE;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.HEADERS;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.METHOD;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.PATH;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.PROTOCOL_VERSION;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.QUERY;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.STATUS;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
+
+import org.apache.hc.core5.http.ContentType;
+import org.apache.hc.core5.http.HttpHost;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.io.SocketConfig;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.testing.classic.ClassicTestServer;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+
+class TestClassicTestClientAdapter {
+
+ private ClassicTestServer server;
+
+ @BeforeEach
+ void initServer() {
+ this.server = new ClassicTestServer(SocketConfig.custom()
+ .setSoTimeout(5, TimeUnit.SECONDS)
+ .build());
+ }
+
+ @AfterEach
+ void shutDownServer() {
+ if (this.server != null) {
+ this.server.shutdown(CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void executeBuildsRequestAndParsesResponse() throws Exception {
+ final Map captured = new HashMap<>();
+ server.register("*", (request, response, context) -> {
+ captured.put("method", request.getMethod());
+ captured.put("version", request.getVersion());
+ final URI requestUri;
+ try {
+ requestUri = request.getUri();
+ } catch (final URISyntaxException ex) {
+ captured.put("uri-error", ex);
+ response.setEntity(new StringEntity("bad uri", ContentType.TEXT_PLAIN));
+ return;
+ }
+ captured.put("path", requestUri.getPath());
+ captured.put("query", requestUri.getQuery());
+ captured.put("header", request.getFirstHeader("X-Test") != null
+ ? request.getFirstHeader("X-Test").getValue()
+ : null);
+ if (request.getEntity() != null) {
+ captured.put("content-type", request.getEntity().getContentType());
+ captured.put("body", EntityUtils.toString(request.getEntity()));
+ } else {
+ captured.put("content-type", null);
+ captured.put("body", null);
+ }
+
+ response.addHeader("X-Reply", "ok");
+ response.setEntity(new StringEntity("pong", ContentType.TEXT_PLAIN));
+ });
+
+ server.start();
+ final String defaultURI = new HttpHost("localhost", server.getPort()).toString();
+
+ final Map request = new HashMap<>();
+ request.put(PATH, "echo");
+ request.put(METHOD, Method.POST.name());
+ request.put(BODY, "ping");
+ request.put(CONTENT_TYPE, ContentType.TEXT_PLAIN.toString());
+ request.put(PROTOCOL_VERSION, HttpVersion.HTTP_1_0);
+ final Map headers = new HashMap<>();
+ headers.put("X-Test", "value");
+ request.put(HEADERS, headers);
+ final Map query = new HashMap<>();
+ query.put("a", "b");
+ query.put("c", "d");
+ request.put(QUERY, query);
+
+ final ClassicTestClientAdapter adapter = new ClassicTestClientAdapter();
+ final Map response = adapter.execute(defaultURI, request);
+
+ Assertions.assertNull(captured.get("uri-error"));
+ Assertions.assertEquals(Method.POST.name(), captured.get("method"));
+ final Object version = captured.get("version");
+ Assertions.assertTrue(HttpVersion.HTTP_1_0.equals(version) || HttpVersion.HTTP_1_1.equals(version));
+ final String path = (String) captured.get("path");
+ Assertions.assertNotNull(path);
+ Assertions.assertTrue(path.endsWith("/echo"));
+ final String queryString = (String) captured.get("query");
+ Assertions.assertNotNull(queryString);
+ Assertions.assertTrue(queryString.contains("a=b"));
+ Assertions.assertTrue(queryString.contains("c=d"));
+ Assertions.assertEquals("value", captured.get("header"));
+ final String capturedContentType = (String) captured.get("content-type");
+ Assertions.assertNotNull(capturedContentType);
+ Assertions.assertTrue(capturedContentType.contains("text/plain"));
+ Assertions.assertEquals("ping", captured.get("body"));
+
+ Assertions.assertEquals(200, response.get(STATUS));
+ Assertions.assertEquals("pong", response.get(BODY));
+ final String contentType = (String) response.get(CONTENT_TYPE);
+ Assertions.assertNotNull(contentType);
+ Assertions.assertTrue(contentType.contains("text/plain"));
+ @SuppressWarnings("unchecked")
+ final Map responseHeaders = (Map) response.get(HEADERS);
+ Assertions.assertEquals("ok", responseHeaders.get("X-Reply"));
+ }
+
+ @Test
+ void executeWithoutEntityReturnsNullBody() throws Exception {
+ server.register("/empty", (request, response, context) -> {
+ Assertions.assertNull(request.getEntity());
+ response.addHeader("X-Empty", "yes");
+ response.setCode(204);
+ });
+
+ server.start();
+ final String defaultURI = new HttpHost("localhost", server.getPort()).toString();
+
+ final Map request = new HashMap<>();
+ request.put(PATH, "empty");
+ request.put(METHOD, Method.GET.name());
+
+ final ClassicTestClientAdapter adapter = new ClassicTestClientAdapter();
+ final Map response = adapter.execute(defaultURI, request);
+
+ Assertions.assertEquals(204, response.get(STATUS));
+ Assertions.assertNull(response.get(BODY));
+ Assertions.assertNull(response.get(CONTENT_TYPE));
+ @SuppressWarnings("unchecked")
+ final Map responseHeaders = (Map) response.get(HEADERS);
+ Assertions.assertEquals("yes", responseHeaders.get("X-Empty"));
+ }
+
+ @Test
+ void getClientName() {
+ final ClassicTestClientAdapter adapter = new ClassicTestClientAdapter();
+ Assertions.assertEquals("ClassicTestClient", adapter.getClientName());
+ }
+
+}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClientTestingAdapter.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClientTestingAdapter.java
index 0af0302de7..c5fd26f978 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClientTestingAdapter.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestClientTestingAdapter.java
@@ -26,6 +26,7 @@
*/
package org.apache.hc.core5.testing.framework;
+import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.Assertions;
@@ -50,4 +51,62 @@ void isRequestSupported() {
Assertions.assertTrue(adapter.isRequestSupported(request), "isRequestSupported should return true");
}
+
+ @Test
+ void executeCallsAssertNothingThrownWhenEnabled() throws Exception {
+ final ClientPOJOAdapter pojoAdapter = new ClientPOJOAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request) {
+ return new HashMap<>();
+ }
+
+ @Override
+ public String getClientName() {
+ return "test";
+ }
+ };
+ final ClientTestingAdapter adapter = new ClientTestingAdapter(pojoAdapter) {
+ {
+ callAssertNothingThrown = true;
+ }
+ };
+
+ final TestingFrameworkRequestHandler requestHandler = new TestingFrameworkRequestHandler() {
+ };
+
+ adapter.execute("http://localhost", new HashMap<>(), requestHandler, new HashMap<>());
+
+ Assertions.assertDoesNotThrow(requestHandler::assertNothingThrown);
+ }
+
+ @Test
+ void executeThrowsWhenHandlerNullAndAssertEnabled() {
+ final ClientPOJOAdapter pojoAdapter = new ClientPOJOAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request) {
+ return new HashMap<>();
+ }
+
+ @Override
+ public String getClientName() {
+ return "test";
+ }
+ };
+ final ClientTestingAdapter adapter = new ClientTestingAdapter(pojoAdapter) {
+ {
+ callAssertNothingThrown = true;
+ }
+ };
+
+ Assertions.assertThrows(TestingFrameworkException.class, () ->
+ adapter.execute("http://localhost", new HashMap<>(), null, new HashMap<>()));
+ }
+
+ @Test
+ void executeThrowsWhenAdapterNull() {
+ final ClientTestingAdapter adapter = new ClientTestingAdapter();
+
+ Assertions.assertThrows(TestingFrameworkException.class, () ->
+ adapter.execute("http://localhost", new HashMap<>(), new TestingFrameworkRequestHandler(), new HashMap<>()));
+ }
}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java
index ccf0f7ce94..8487be8561 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFramework.java
@@ -1022,4 +1022,159 @@ public Map execute(final String defaultURI, final Map TestingFramework.deepcopy(new Object()));
+ }
+
+ @Test
+ void addTestUsesDeepCopyAndLocksExpectations() throws Exception {
+ final Map test = new HashMap<>();
+ final Map request = new HashMap<>();
+ request.put(METHOD, "POST");
+ request.put(PATH, "copy");
+ test.put(REQUEST, request);
+
+ final Map response = new HashMap<>();
+ response.put(STATUS, 200);
+ response.put(BODY, "ok");
+ response.put(CONTENT_TYPE, "text/plain");
+ final Map headers = new HashMap<>();
+ headers.put("X-A", "v1");
+ response.put(HEADERS, headers);
+ test.put(RESPONSE, response);
+
+ final ClientTestingAdapter adapter = new ClientTestingAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request,
+ final TestingFrameworkRequestHandler requestHandler,
+ final Map responseExpectations) throws TestingFrameworkException {
+ Assertions.assertEquals("POST", request.get(METHOD));
+ Assertions.assertEquals("copy", request.get(PATH));
+ Assertions.assertEquals(200, responseExpectations.get(STATUS));
+ Assertions.assertEquals("ok", responseExpectations.get(BODY));
+ Assertions.assertEquals("text/plain", responseExpectations.get(CONTENT_TYPE));
+ @SuppressWarnings("unchecked")
+ final Map expectedHeaders = (Map) responseExpectations.get(HEADERS);
+ Assertions.assertEquals("v1", expectedHeaders.get("X-A"));
+ Assertions.assertThrows(UnsupportedOperationException.class, () -> responseExpectations.put("x", "y"));
+
+ final Map actual = new HashMap<>();
+ actual.put(STATUS, responseExpectations.get(STATUS));
+ actual.put(BODY, responseExpectations.get(BODY));
+ actual.put(CONTENT_TYPE, responseExpectations.get(CONTENT_TYPE));
+ actual.put(HEADERS, responseExpectations.get(HEADERS));
+ return actual;
+ }
+ };
+
+ final TestingFramework framework = newWebServerTestingFramework(adapter);
+ framework.addTest(test);
+
+ request.put(METHOD, "GET");
+ response.put(STATUS, 500);
+
+ framework.runTests();
+ }
+
+ @Test
+ void headSkipsBodyAndContentTypeChecks() throws Exception {
+ final Map test = new HashMap<>();
+ final Map request = new HashMap<>();
+ request.put(METHOD, "HEAD");
+ request.put(PATH, "head");
+ test.put(REQUEST, request);
+
+ final Map response = new HashMap<>();
+ response.put(STATUS, 200);
+ response.put(BODY, "ignored");
+ response.put(CONTENT_TYPE, "text/plain");
+ final Map headers = new HashMap<>();
+ headers.put("X-Ok", "yes");
+ response.put(HEADERS, headers);
+ test.put(RESPONSE, response);
+
+ final ClientTestingAdapter adapter = new ClientTestingAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request,
+ final TestingFrameworkRequestHandler requestHandler,
+ final Map responseExpectations) {
+ final Map actual = new HashMap<>();
+ actual.put(STATUS, 200);
+ actual.put(HEADERS, responseExpectations.get(HEADERS));
+ return actual;
+ }
+ };
+
+ final TestingFramework framework = newWebServerTestingFramework(adapter);
+ framework.addTest(test);
+ framework.runTests();
+ }
+
+ @Test
+ void missingHeaderThrows() throws Exception {
+ final Map test = new HashMap<>();
+ final Map request = new HashMap<>();
+ request.put(METHOD, "GET");
+ request.put(PATH, "header-check");
+ test.put(REQUEST, request);
+
+ final Map response = new HashMap<>();
+ response.put(STATUS, 200);
+ final Map headers = new HashMap<>();
+ headers.put("X-Need", "value");
+ response.put(HEADERS, headers);
+ test.put(RESPONSE, response);
+
+ final ClientTestingAdapter adapter = new ClientTestingAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request,
+ final TestingFrameworkRequestHandler requestHandler,
+ final Map responseExpectations) {
+ final Map actual = new HashMap<>();
+ actual.put(STATUS, 200);
+ actual.put(HEADERS, new HashMap());
+ return actual;
+ }
+ };
+
+ final TestingFramework framework = newWebServerTestingFramework(adapter);
+ framework.addTest(test);
+ Assertions.assertThrows(TestingFrameworkException.class, framework::runTests);
+ }
+
+ @Test
+ void nullResponseThrows() throws Exception {
+ final ClientTestingAdapter adapter = new ClientTestingAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request,
+ final TestingFrameworkRequestHandler requestHandler,
+ final Map responseExpectations) {
+ return null;
+ }
+ };
+
+ final TestingFramework framework = newWebServerTestingFramework(adapter);
+ framework.addTest();
+ Assertions.assertThrows(TestingFrameworkException.class, framework::runTests);
+ }
+
+ @Test
+ void nullStatusThrows() throws Exception {
+ final ClientTestingAdapter adapter = new ClientTestingAdapter() {
+ @Override
+ public Map execute(final String defaultURI, final Map request,
+ final TestingFrameworkRequestHandler requestHandler,
+ final Map responseExpectations) {
+ final Map response = new HashMap<>();
+ response.put(STATUS, null);
+ return response;
+ }
+ };
+
+ final TestingFramework framework = newWebServerTestingFramework(adapter);
+ framework.addTest();
+ Assertions.assertThrows(TestingFrameworkException.class, framework::runTests);
+ }
+
}
\ No newline at end of file
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFrameworkRequestHandler.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFrameworkRequestHandler.java
index 04c2e86a85..39f5a60a82 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFrameworkRequestHandler.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/framework/TestTestingFrameworkRequestHandler.java
@@ -27,12 +27,30 @@
package org.apache.hc.core5.testing.framework;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.BODY;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.CONTENT_TYPE;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.HEADERS;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.METHOD;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.PROTOCOL_VERSION;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.QUERY;
+import static org.apache.hc.core5.testing.framework.ClientPOJOAdapter.STATUS;
+
import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
import org.apache.hc.core5.http.ClassicHttpRequest;
import org.apache.hc.core5.http.ClassicHttpResponse;
+import org.apache.hc.core5.http.ContentType;
import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpVersion;
+import org.apache.hc.core5.http.Method;
+import org.apache.hc.core5.http.io.entity.EntityUtils;
+import org.apache.hc.core5.http.io.entity.StringEntity;
+import org.apache.hc.core5.http.message.BasicClassicHttpRequest;
+import org.apache.hc.core5.http.message.BasicClassicHttpResponse;
import org.apache.hc.core5.http.protocol.HttpContext;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -71,4 +89,68 @@ public void handle(final ClassicHttpRequest request, final ClassicHttpResponse r
handler.assertNothingThrown();
}
+ @Test
+ void handleValidatesRequestAndBuildsResponse() throws Exception {
+ final TestingFrameworkRequestHandler handler = new TestingFrameworkRequestHandler();
+
+ final Map requestExpectations = new HashMap<>();
+ requestExpectations.put(METHOD, Method.POST.name());
+ requestExpectations.put(BODY, "ping");
+ requestExpectations.put(CONTENT_TYPE, ContentType.TEXT_PLAIN.toString());
+ requestExpectations.put(PROTOCOL_VERSION, HttpVersion.HTTP_1_1);
+ final Map query = new HashMap<>();
+ query.put("q", "1");
+ query.put("r", "2");
+ requestExpectations.put(QUERY, query);
+ final Map headers = new HashMap<>();
+ headers.put("X-Test", "value");
+ requestExpectations.put(HEADERS, headers);
+ handler.setRequestExpectations(requestExpectations);
+
+ final Map desiredResponse = new HashMap<>();
+ desiredResponse.put(STATUS, 201);
+ desiredResponse.put(BODY, "pong");
+ desiredResponse.put(CONTENT_TYPE, ContentType.TEXT_PLAIN.toString());
+ final Map responseHeaders = new HashMap<>();
+ responseHeaders.put("X-Reply", "ok");
+ desiredResponse.put(HEADERS, responseHeaders);
+ handler.setDesiredResponse(desiredResponse);
+
+ final BasicClassicHttpRequest request = new BasicClassicHttpRequest(Method.POST, "/echo?q=1&r=2");
+ request.setVersion(HttpVersion.HTTP_1_1);
+ request.addHeader("X-Test", "value");
+ request.setEntity(new StringEntity("ping", ContentType.TEXT_PLAIN));
+ final ClassicHttpResponse response = new BasicClassicHttpResponse(200);
+
+ handler.handle(request, response, HttpCoreContext.create());
+ handler.assertNothingThrown();
+
+ Assertions.assertEquals(201, response.getCode());
+ Assertions.assertEquals("pong", EntityUtils.toString(response.getEntity()));
+ Assertions.assertTrue(response.getEntity().getContentType().contains("text/plain"));
+ Assertions.assertEquals("ok", response.getFirstHeader("X-Reply").getValue());
+ }
+
+ @Test
+ void handleStoresThrowableOnMismatch() throws Exception {
+ final TestingFrameworkRequestHandler handler = new TestingFrameworkRequestHandler();
+
+ final Map requestExpectations = new HashMap<>();
+ requestExpectations.put(METHOD, Method.GET.name());
+ final Map headers = new HashMap<>();
+ headers.put("X-Needed", "true");
+ requestExpectations.put(HEADERS, headers);
+ handler.setRequestExpectations(requestExpectations);
+
+ final Map desiredResponse = new HashMap<>();
+ desiredResponse.put(STATUS, 200);
+ handler.setDesiredResponse(desiredResponse);
+
+ final ClassicHttpRequest request = new BasicClassicHttpRequest(Method.GET, "/");
+ final ClassicHttpResponse response = new BasicClassicHttpResponse(200);
+
+ handler.handle(request, response, HttpCoreContext.create());
+ Assertions.assertThrows(TestingFrameworkException.class, handler::assertNothingThrown);
+ }
+
}
diff --git a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java
index 73b87d2cad..a862b3ac3d 100644
--- a/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java
+++ b/httpcore5-testing/src/test/java/org/apache/hc/core5/testing/nio/HttpIntegrationTest.java
@@ -383,6 +383,173 @@ void testLargePostsPipelined() throws Exception {
}
}
+ @Test
+ void testEchoEmptyEntity() throws Exception {
+ final HttpTestServer server = server();
+ final HttpTestClient client = client();
+
+ server.register("*", () -> new EchoHandler(8));
+ final InetSocketAddress serverEndpoint = server.start();
+
+ final HttpHost target = target(serverEndpoint);
+
+ client.start();
+ final Future connectFuture = client.connect(target, TIMEOUT);
+ final ClientSessionEndpoint streamEndpoint = connectFuture.get();
+
+ final BasicHttpRequest request = BasicRequestBuilder.post()
+ .setHttpHost(target)
+ .setPath("/echo")
+ .build();
+ final Future> future = streamEndpoint.execute(
+ new BasicRequestProducer(request, AsyncEntityProducers.create("")),
+ new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null);
+
+ final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertNotNull(result);
+ final HttpResponse response = result.getHead();
+ final String entity = result.getBody();
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ Assertions.assertNotNull(entity);
+ Assertions.assertEquals("", entity);
+ }
+
+ @Test
+ void testEchoLargeEntitySmallBuffer() throws Exception {
+ final HttpTestServer server = server();
+ final HttpTestClient client = client();
+
+ server.register("*", () -> new EchoHandler(8));
+ final InetSocketAddress serverEndpoint = server.start();
+
+ final HttpHost target = target(serverEndpoint);
+
+ client.start();
+ final Future connectFuture = client.connect(target, TIMEOUT);
+ final ClientSessionEndpoint streamEndpoint = connectFuture.get();
+
+ final StringBuilder builder = new StringBuilder();
+ for (int i = 0; i < 5000; i++) {
+ builder.append("0123456789abcdef");
+ }
+ final String content = builder.toString();
+
+ final BasicHttpRequest request = BasicRequestBuilder.post()
+ .setHttpHost(target)
+ .setPath("/echo")
+ .build();
+ final Future> future = streamEndpoint.execute(
+ new BasicRequestProducer(request, AsyncEntityProducers.create(content)),
+ new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null);
+
+ final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertNotNull(result);
+ final HttpResponse response = result.getHead();
+ final String entity = result.getBody();
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ Assertions.assertEquals(content, entity);
+ }
+
+ @Test
+ void testEchoNoEntityRequest() throws Exception {
+ final HttpTestServer server = server();
+ final HttpTestClient client = client();
+
+ server.register("*", () -> new EchoHandler(8));
+ final InetSocketAddress serverEndpoint = server.start();
+
+ final HttpHost target = target(serverEndpoint);
+
+ client.start();
+ final Future connectFuture = client.connect(target, TIMEOUT);
+ final ClientSessionEndpoint streamEndpoint = connectFuture.get();
+
+ final BasicHttpRequest request = BasicRequestBuilder.post()
+ .setHttpHost(target)
+ .setPath("/echo")
+ .build();
+ final Future> future = streamEndpoint.execute(
+ new BasicRequestProducer(request, null),
+ new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null);
+
+ final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertNotNull(result);
+ final HttpResponse response = result.getHead();
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ final String body = result.getBody();
+ Assertions.assertTrue(body == null || body.isEmpty());
+ }
+
+ @Test
+ void testEchoHeadNoBody() throws Exception {
+ final HttpTestServer server = server();
+ final HttpTestClient client = client();
+
+ server.register("*", () -> new EchoHandler(8));
+ final InetSocketAddress serverEndpoint = server.start();
+
+ final HttpHost target = target(serverEndpoint);
+
+ client.start();
+ final Future connectFuture = client.connect(target, TIMEOUT);
+ final ClientSessionEndpoint streamEndpoint = connectFuture.get();
+
+ final BasicHttpRequest request = BasicRequestBuilder.head()
+ .setHttpHost(target)
+ .setPath("/echo")
+ .build();
+ final Future> future = streamEndpoint.execute(
+ new BasicRequestProducer(request, null),
+ new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null);
+
+ final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertNotNull(result);
+ final HttpResponse response = result.getHead();
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ Assertions.assertNull(result.getBody());
+ }
+
+ @Test
+ void testEchoManySmallRequests() throws Exception {
+ final HttpTestServer server = server();
+ final HttpTestClient client = client();
+
+ server.register("*", () -> new EchoHandler(8));
+ final InetSocketAddress serverEndpoint = server.start();
+
+ final HttpHost target = target(serverEndpoint);
+
+ client.start();
+ final Future connectFuture = client.connect(target, TIMEOUT);
+ final ClientSessionEndpoint streamEndpoint = connectFuture.get();
+
+ final Queue>> queue = new LinkedList<>();
+ for (int i = 0; i < REQ_NUM; i++) {
+ final BasicHttpRequest request = BasicRequestBuilder.post()
+ .setHttpHost(target)
+ .setPath("/echo")
+ .build();
+ queue.add(streamEndpoint.execute(
+ new BasicRequestProducer(request, AsyncEntityProducers.create("hi-" + i)),
+ new BasicResponseConsumer<>(new StringAsyncEntityConsumer()), null));
+ }
+
+ for (int i = 0; i < REQ_NUM; i++) {
+ final Future> future = queue.remove();
+ final Message result = future.get(TIMEOUT.getDuration(), TIMEOUT.getTimeUnit());
+ Assertions.assertNotNull(result);
+ final HttpResponse response = result.getHead();
+ final String entity = result.getBody();
+ Assertions.assertNotNull(response);
+ Assertions.assertEquals(HttpStatus.SC_OK, response.getCode());
+ Assertions.assertEquals("hi-" + i, entity);
+ }
+ }
+
@Test
void testNoEntityPost() throws Exception {
final HttpTestServer server = server();
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestRequestExecutionCommand.java b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestRequestExecutionCommand.java
new file mode 100644
index 0000000000..970cdb0731
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestRequestExecutionCommand.java
@@ -0,0 +1,107 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http.nio.command;
+
+import org.apache.hc.core5.concurrent.CancellableDependency;
+import org.apache.hc.core5.function.Callback;
+import org.apache.hc.core5.http.RequestNotExecutedException;
+import org.apache.hc.core5.http.StreamControl;
+import org.apache.hc.core5.http.nio.AsyncClientExchangeHandler;
+import org.apache.hc.core5.http.nio.AsyncPushConsumer;
+import org.apache.hc.core5.http.nio.HandlerFactory;
+import org.apache.hc.core5.http.protocol.HttpCoreContext;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestRequestExecutionCommand {
+
+ @Test
+ void initiatedCallsCallback() {
+ final AsyncClientExchangeHandler handler = Mockito.mock(AsyncClientExchangeHandler.class);
+ @SuppressWarnings("unchecked")
+ final Callback callback = Mockito.mock(Callback.class);
+ final RequestExecutionCommand command = new RequestExecutionCommand(handler, null, HttpCoreContext.create(), callback);
+ final StreamControl streamControl = Mockito.mock(StreamControl.class);
+
+ command.initiated(streamControl);
+
+ Mockito.verify(callback).execute(streamControl);
+ }
+
+ @Test
+ @SuppressWarnings("deprecation")
+ void initiatedUpdatesDeprecatedDependency() {
+ final AsyncClientExchangeHandler handler = Mockito.mock(AsyncClientExchangeHandler.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushFactory = Mockito.mock(HandlerFactory.class);
+ final CancellableDependency dependency = Mockito.mock(CancellableDependency.class);
+ final RequestExecutionCommand command = new RequestExecutionCommand(handler, pushFactory, dependency, HttpCoreContext.create());
+ final StreamControl streamControl = Mockito.mock(StreamControl.class);
+
+ command.initiated(streamControl);
+
+ Mockito.verify(dependency).setDependency(streamControl);
+ }
+
+ @Test
+ void failedInvokesHandlerOnce() {
+ final AsyncClientExchangeHandler handler = Mockito.mock(AsyncClientExchangeHandler.class);
+ final RequestExecutionCommand command = new RequestExecutionCommand(handler, HttpCoreContext.create());
+
+ command.failed(new RuntimeException("boom"));
+ command.failed(new RuntimeException("again"));
+
+ Mockito.verify(handler).failed(Mockito.any(Exception.class));
+ Mockito.verify(handler).releaseResources();
+ }
+
+ @Test
+ void cancelInvokesNotExecutedExceptionOnce() {
+ final AsyncClientExchangeHandler handler = Mockito.mock(AsyncClientExchangeHandler.class);
+ final RequestExecutionCommand command = new RequestExecutionCommand(handler, HttpCoreContext.create());
+
+ Assertions.assertTrue(command.cancel());
+ Assertions.assertFalse(command.cancel());
+
+ Mockito.verify(handler).failed(Mockito.isA(RequestNotExecutedException.class));
+ Mockito.verify(handler).releaseResources();
+ }
+
+ @Test
+ void gettersExposeConstructorValues() {
+ final AsyncClientExchangeHandler handler = Mockito.mock(AsyncClientExchangeHandler.class);
+ @SuppressWarnings("unchecked")
+ final HandlerFactory pushFactory = Mockito.mock(HandlerFactory.class);
+ final RequestExecutionCommand command = new RequestExecutionCommand(handler, pushFactory, HttpCoreContext.create());
+
+ Assertions.assertSame(handler, command.getExchangeHandler());
+ Assertions.assertSame(pushFactory, command.getPushHandlerFactory());
+ Assertions.assertNotNull(command.getContext());
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestShutdownCommand.java b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestShutdownCommand.java
new file mode 100644
index 0000000000..82e7e6e716
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestShutdownCommand.java
@@ -0,0 +1,61 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http.nio.command;
+
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.reactor.Command;
+import org.apache.hc.core5.reactor.IOSession;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestShutdownCommand {
+
+ @Test
+ void cancelAlwaysReturnsTrue() {
+ final ShutdownCommand command = new ShutdownCommand(CloseMode.GRACEFUL);
+ Assertions.assertTrue(command.cancel());
+ }
+
+ @Test
+ void callbacksEnqueueGracefulShutdown() {
+ final IOSession session = Mockito.mock(IOSession.class);
+
+ ShutdownCommand.GRACEFUL_IMMEDIATE_CALLBACK.execute(session);
+ ShutdownCommand.GRACEFUL_NORMAL_CALLBACK.execute(session);
+
+ Mockito.verify(session).enqueue(ShutdownCommand.GRACEFUL, Command.Priority.IMMEDIATE);
+ Mockito.verify(session).enqueue(ShutdownCommand.GRACEFUL, Command.Priority.NORMAL);
+ }
+
+ @Test
+ void toStringIncludesType() {
+ final ShutdownCommand command = new ShutdownCommand(CloseMode.IMMEDIATE);
+ Assertions.assertTrue(command.toString().contains("IMMEDIATE"));
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestStaleCheckCommand.java b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestStaleCheckCommand.java
new file mode 100644
index 0000000000..d37eb2f1da
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/nio/command/TestStaleCheckCommand.java
@@ -0,0 +1,55 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http.nio.command;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestStaleCheckCommand {
+
+ @Test
+ void cancelInvokesCallbackFalse() {
+ final AtomicReference called = new AtomicReference<>();
+ final StaleCheckCommand command = new StaleCheckCommand(called::set);
+
+ Assertions.assertTrue(command.cancel());
+ Assertions.assertEquals(Boolean.FALSE, called.get());
+ }
+
+ @Test
+ void exposesCallback() {
+ final AtomicReference called = new AtomicReference<>();
+ final StaleCheckCommand command = new StaleCheckCommand(called::set);
+
+ command.getCallback().accept(Boolean.TRUE);
+
+ Assertions.assertEquals(Boolean.TRUE, called.get());
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestDefaultHttpProcessor.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestDefaultHttpProcessor.java
new file mode 100644
index 0000000000..d572c46eb6
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestDefaultHttpProcessor.java
@@ -0,0 +1,121 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http.protocol;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.hc.core5.http.EntityDetails;
+import org.apache.hc.core5.http.HttpException;
+import org.apache.hc.core5.http.HttpRequest;
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponse;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestDefaultHttpProcessor {
+
+ @Test
+ void processesRequestAndResponseInterceptorsInOrder() throws Exception {
+ final List events = new ArrayList<>();
+ final HttpRequestInterceptor r1 = (request, entity, context) -> events.add("r1");
+ final HttpRequestInterceptor r2 = (request, entity, context) -> events.add("r2");
+ final HttpResponseInterceptor s1 = (response, entity, context) -> events.add("s1");
+ final HttpResponseInterceptor s2 = (response, entity, context) -> events.add("s2");
+
+ final DefaultHttpProcessor processor = new DefaultHttpProcessor(
+ new HttpRequestInterceptor[] { r1, r2 },
+ new HttpResponseInterceptor[] { s1, s2 });
+
+ processor.process(new BasicHttpRequest("GET", "/"), null, HttpCoreContext.create());
+ processor.process(new BasicHttpResponse(200), null, HttpCoreContext.create());
+
+ Assertions.assertEquals(Arrays.asList("r1", "r2", "s1", "s2"), events);
+ }
+
+ @Test
+ void copiesInterceptorArrays() throws Exception {
+ final List events = new ArrayList<>();
+ final HttpRequestInterceptor original = (request, entity, context) -> events.add("orig");
+ final HttpRequestInterceptor replacement = (request, entity, context) -> events.add("new");
+ final HttpRequestInterceptor[] requestInterceptors = new HttpRequestInterceptor[] { original };
+
+ final DefaultHttpProcessor processor = new DefaultHttpProcessor(requestInterceptors, null);
+ requestInterceptors[0] = replacement;
+
+ processor.process(new BasicHttpRequest("GET", "/"), null, HttpCoreContext.create());
+
+ Assertions.assertEquals(Arrays.asList("orig"), events);
+ }
+
+ @Test
+ void handlesEmptyInterceptors() throws Exception {
+ final DefaultHttpProcessor processor = new DefaultHttpProcessor((HttpRequestInterceptor[]) null);
+ final HttpRequest request = new BasicHttpRequest("GET", "/");
+ final HttpResponse response = new BasicHttpResponse(200);
+ final EntityDetails entity = null;
+
+ processor.process(request, entity, HttpCoreContext.create());
+ processor.process(response, entity, HttpCoreContext.create());
+ }
+
+ @Test
+ void listConstructorBuildsChains() throws Exception {
+ final List events = new ArrayList<>();
+ final List req = Arrays.asList(
+ (r, e, c) -> events.add("r1"),
+ (r, e, c) -> events.add("r2"));
+ final List res = Arrays.asList(
+ (r, e, c) -> events.add("s1"));
+
+ final DefaultHttpProcessor processor = new DefaultHttpProcessor(req, res);
+ processor.process(new BasicHttpRequest("GET", "/"), null, HttpCoreContext.create());
+ processor.process(new BasicHttpResponse(200), null, HttpCoreContext.create());
+
+ Assertions.assertEquals(Arrays.asList("r1", "r2", "s1"), events);
+ }
+
+ @Test
+ void exceptionsPropagate() {
+ final HttpRequestInterceptor interceptor = new HttpRequestInterceptor() {
+ @Override
+ public void process(final HttpRequest request, final EntityDetails entity, final HttpContext context)
+ throws HttpException {
+ throw new HttpException("boom");
+ }
+ };
+ final DefaultHttpProcessor processor = new DefaultHttpProcessor(interceptor);
+
+ Assertions.assertThrows(HttpException.class, () ->
+ processor.process(new BasicHttpRequest("GET", "/"), null, HttpCoreContext.create()));
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestHttpProcessorBuilder.java b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestHttpProcessorBuilder.java
new file mode 100644
index 0000000000..e9f54a0097
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/http/protocol/TestHttpProcessorBuilder.java
@@ -0,0 +1,85 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.http.protocol;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+import org.apache.hc.core5.http.HttpRequestInterceptor;
+import org.apache.hc.core5.http.HttpResponseInterceptor;
+import org.apache.hc.core5.http.message.BasicHttpRequest;
+import org.apache.hc.core5.http.message.BasicHttpResponse;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestHttpProcessorBuilder {
+
+ @Test
+ void buildsRequestChainInOrder() throws Exception {
+ final List events = new ArrayList<>();
+ final HttpRequestInterceptor r1 = (r, e, c) -> events.add("r1");
+ final HttpRequestInterceptor r2 = (r, e, c) -> events.add("r2");
+ final HttpRequestInterceptor r3 = (r, e, c) -> events.add("r3");
+
+ final HttpProcessor processor = HttpProcessorBuilder.create()
+ .addFirst(r2)
+ .addLast(r3)
+ .addFirst(r1)
+ .add((HttpRequestInterceptor) null)
+ .build();
+
+ processor.process(new BasicHttpRequest("GET", "/"), null, HttpCoreContext.create());
+
+ Assertions.assertEquals(Arrays.asList("r1", "r2", "r3"), events);
+ }
+
+ @Test
+ void buildsResponseChainInOrder() throws Exception {
+ final List events = new ArrayList<>();
+ final HttpResponseInterceptor s1 = (r, e, c) -> events.add("s1");
+ final HttpResponseInterceptor s2 = (r, e, c) -> events.add("s2");
+
+ final HttpProcessor processor = HttpProcessorBuilder.create()
+ .addAllFirst(s2)
+ .addLast(s1)
+ .addAll((HttpResponseInterceptor[]) null)
+ .build();
+
+ processor.process(new BasicHttpResponse(200), null, HttpCoreContext.create());
+
+ Assertions.assertEquals(Arrays.asList("s2", "s1"), events);
+ }
+
+ @Test
+ void buildsEmptyProcessor() throws Exception {
+ final HttpProcessor processor = HttpProcessorBuilder.create().build();
+ processor.process(new BasicHttpRequest("GET", "/"), null, HttpCoreContext.create());
+ processor.process(new BasicHttpResponse(200), null, HttpCoreContext.create());
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/pool/TestDefaultDisposalCallback.java b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestDefaultDisposalCallback.java
new file mode 100644
index 0000000000..59d96e2fce
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/pool/TestDefaultDisposalCallback.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.pool;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.http.SocketModalCloseable;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestDefaultDisposalCallback {
+
+ private static class TestCloseable implements SocketModalCloseable {
+ private Timeout socketTimeout;
+ private final AtomicReference closedWith = new AtomicReference<>();
+
+ TestCloseable(final Timeout socketTimeout) {
+ this.socketTimeout = socketTimeout;
+ }
+
+ @Override
+ public void close() {
+ close(CloseMode.GRACEFUL);
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ closedWith.set(closeMode);
+ }
+
+ @Override
+ public Timeout getSocketTimeout() {
+ return socketTimeout;
+ }
+
+ @Override
+ public void setSocketTimeout(final Timeout timeout) {
+ socketTimeout = timeout;
+ }
+ }
+
+ @Test
+ void setsDefaultTimeoutWhenMissingOrTooLarge() {
+ final DefaultDisposalCallback callback = new DefaultDisposalCallback<>();
+ final TestCloseable closeable = new TestCloseable(null);
+
+ callback.execute(closeable, CloseMode.IMMEDIATE);
+
+ Assertions.assertEquals(Timeout.ofSeconds(1), closeable.getSocketTimeout());
+ Assertions.assertEquals(CloseMode.IMMEDIATE, closeable.closedWith.get());
+
+ final TestCloseable closeableTooLarge = new TestCloseable(Timeout.ofSeconds(5));
+ callback.execute(closeableTooLarge, CloseMode.GRACEFUL);
+ Assertions.assertEquals(Timeout.ofSeconds(1), closeableTooLarge.getSocketTimeout());
+ Assertions.assertEquals(CloseMode.GRACEFUL, closeableTooLarge.closedWith.get());
+ }
+
+ @Test
+ void keepsReasonableTimeout() {
+ final DefaultDisposalCallback callback = new DefaultDisposalCallback<>();
+ final TestCloseable closeable = new TestCloseable(Timeout.ofMilliseconds(500));
+
+ callback.execute(closeable, CloseMode.GRACEFUL);
+
+ Assertions.assertEquals(Timeout.ofMilliseconds(500), closeable.getSocketTimeout());
+ Assertions.assertEquals(CloseMode.GRACEFUL, closeable.closedWith.get());
+ }
+
+ @Test
+ void normalizesZeroOrNegativeTimeout() {
+ final DefaultDisposalCallback callback = new DefaultDisposalCallback<>();
+ final TestCloseable closeable = new TestCloseable(Timeout.ofMilliseconds(0));
+
+ callback.execute(closeable, CloseMode.IMMEDIATE);
+
+ Assertions.assertEquals(Timeout.ofSeconds(1), closeable.getSocketTimeout());
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestAbstractIOReactorBase.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestAbstractIOReactorBase.java
new file mode 100644
index 0000000000..d5e256db32
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestAbstractIOReactorBase.java
@@ -0,0 +1,174 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.concurrent.Future;
+
+import org.apache.hc.core5.concurrent.BasicFuture;
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.util.TimeValue;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestAbstractIOReactorBase {
+
+ private static class StubSingleCoreIOReactor extends SingleCoreIOReactor {
+ private final Future future;
+ private boolean connectCalled;
+
+ StubSingleCoreIOReactor(final Future future) {
+ super(null, Mockito.mock(IOEventHandlerFactory.class), IOReactorConfig.DEFAULT, null, null, null, null);
+ this.future = future;
+ }
+
+ @Override
+ public Future connect(final NamedEndpoint remoteEndpoint, final SocketAddress remoteAddress,
+ final SocketAddress localAddress, final Timeout timeout, final Object attachment,
+ final org.apache.hc.core5.concurrent.FutureCallback callback) {
+ connectCalled = true;
+ return future;
+ }
+
+ boolean isConnectCalled() {
+ return connectCalled;
+ }
+ }
+
+ private static class TestReactor extends AbstractIOReactorBase {
+ private IOReactorStatus status;
+ private boolean shutdownCalled;
+ private final SingleCoreIOReactor worker;
+
+ TestReactor(final IOReactorStatus status, final SingleCoreIOReactor worker) {
+ this.status = status;
+ this.worker = worker;
+ }
+
+ @Override
+ SingleCoreIOReactor selectWorker() {
+ return worker;
+ }
+
+ @Override
+ public IOReactorStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public void initiateShutdown() {
+ shutdownCalled = true;
+ status = IOReactorStatus.SHUTTING_DOWN;
+ }
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void awaitShutdown(final TimeValue waitTime) {
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ }
+
+ @Override
+ public void close() {
+ }
+ }
+
+ private static NamedEndpoint endpoint() {
+ return new NamedEndpoint() {
+ @Override
+ public String getHostName() {
+ return "localhost";
+ }
+
+ @Override
+ public int getPort() {
+ return 80;
+ }
+ };
+ }
+
+ @Test
+ void connectFailsWhenReactorShuttingDown() {
+ try (StubSingleCoreIOReactor worker = new StubSingleCoreIOReactor(new BasicFuture<>(null))) {
+ final TestReactor reactor = new TestReactor(IOReactorStatus.SHUTTING_DOWN, worker);
+ try {
+ Assertions.assertThrows(IOReactorShutdownException.class, () ->
+ reactor.connect(endpoint(), new InetSocketAddress("localhost", 80), null, Timeout.ofSeconds(1), null, null));
+ } finally {
+ reactor.close();
+ }
+ }
+ }
+
+ @Test
+ void connectFailsWhenWorkerShutdownTriggersShutdown() {
+ try (StubSingleCoreIOReactor worker = new StubSingleCoreIOReactor(new BasicFuture<>(null))) {
+ worker.initiateShutdown();
+ final TestReactor reactor = new TestReactor(IOReactorStatus.ACTIVE, worker);
+ try {
+ Assertions.assertThrows(IOReactorShutdownException.class, () ->
+ reactor.connect(endpoint(), new InetSocketAddress("localhost", 80), null, Timeout.ofSeconds(1), null, null));
+ Assertions.assertTrue(reactor.shutdownCalled);
+ Assertions.assertFalse(worker.isConnectCalled());
+ } finally {
+ reactor.close();
+ }
+ }
+ }
+
+ @Test
+ void connectDelegatesToWorker() throws Exception {
+ final BasicFuture future = new BasicFuture<>(null);
+ try (StubSingleCoreIOReactor worker = new StubSingleCoreIOReactor(future)) {
+ final TestReactor reactor = new TestReactor(IOReactorStatus.INACTIVE, worker);
+ try {
+ final Future result = reactor.connect(
+ endpoint(),
+ new InetSocketAddress("localhost", 80),
+ null,
+ Timeout.ofSeconds(1),
+ null,
+ null);
+
+ Assertions.assertSame(future, result);
+ Assertions.assertTrue(worker.isConnectCalled());
+ } finally {
+ reactor.close();
+ }
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestDefaultConnectingIOReactor.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestDefaultConnectingIOReactor.java
new file mode 100644
index 0000000000..df27afe873
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestDefaultConnectingIOReactor.java
@@ -0,0 +1,105 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.net.InetSocketAddress;
+
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestDefaultConnectingIOReactor {
+
+ private DefaultConnectingIOReactor newReactor(final IOWorkerSelector selector) {
+ final IOEventHandlerFactory factory = Mockito.mock(IOEventHandlerFactory.class);
+ Mockito.when(factory.createHandler(Mockito.any(), Mockito.any())).thenReturn(Mockito.mock(IOEventHandler.class));
+ @SuppressWarnings("unchecked")
+ final Decorator decorator = (Decorator) Mockito.mock(Decorator.class);
+ final IOReactorConfig config = IOReactorConfig.custom().setIoThreadCount(1).build();
+ return new DefaultConnectingIOReactor(
+ factory,
+ config,
+ null,
+ decorator,
+ null,
+ null,
+ null,
+ null,
+ selector);
+ }
+
+ @Test
+ void selectWorkerUsesSelector() {
+ final IOWorkerSelector selector = dispatchers -> 0;
+ final DefaultConnectingIOReactor reactor = newReactor(selector);
+ try {
+ final SingleCoreIOReactor worker = reactor.selectWorker();
+ Assertions.assertNotNull(worker);
+ } finally {
+ reactor.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void connectDelegatesToWorker() throws Exception {
+ final IOWorkerSelector selector = dispatchers -> 0;
+ final DefaultConnectingIOReactor reactor = newReactor(selector);
+ try {
+ final NamedEndpoint endpoint = new NamedEndpoint() {
+ @Override
+ public String getHostName() {
+ return "localhost";
+ }
+
+ @Override
+ public int getPort() {
+ return 80;
+ }
+ };
+
+ reactor.connect(endpoint, new InetSocketAddress("localhost", 80), null, Timeout.ofSeconds(1), null, null);
+
+ Assertions.assertEquals(1, reactor.selectWorker().pendingChannelCount());
+ } finally {
+ reactor.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void statusDelegatesToInnerReactor() {
+ final DefaultConnectingIOReactor reactor = newReactor(dispatchers -> 0);
+ try {
+ Assertions.assertEquals(IOReactorStatus.INACTIVE, reactor.getStatus());
+ } finally {
+ reactor.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestDefaultListeningIOReactor.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestDefaultListeningIOReactor.java
new file mode 100644
index 0000000000..c81ed2109c
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestDefaultListeningIOReactor.java
@@ -0,0 +1,106 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.net.InetSocketAddress;
+
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestDefaultListeningIOReactor {
+
+ private DefaultListeningIOReactor newReactor(final IOWorkerSelector selector) {
+ final IOEventHandlerFactory factory = Mockito.mock(IOEventHandlerFactory.class);
+ Mockito.when(factory.createHandler(Mockito.any(), Mockito.any())).thenReturn(Mockito.mock(IOEventHandler.class));
+ @SuppressWarnings("unchecked")
+ final Decorator decorator = (Decorator) Mockito.mock(Decorator.class);
+ final IOReactorConfig config = IOReactorConfig.custom().setIoThreadCount(1).build();
+ return new DefaultListeningIOReactor(
+ factory,
+ config,
+ null,
+ null,
+ decorator,
+ null,
+ null,
+ null,
+ null,
+ selector);
+ }
+
+ @Test
+ void selectWorkerUsesSelector() {
+ final IOWorkerSelector selector = dispatchers -> 0;
+ final DefaultListeningIOReactor reactor = newReactor(selector);
+ try {
+ final SingleCoreIOReactor worker = reactor.selectWorker();
+ Assertions.assertNotNull(worker);
+ } finally {
+ reactor.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void connectDelegatesToWorker() throws Exception {
+ final IOWorkerSelector selector = dispatchers -> 0;
+ final DefaultListeningIOReactor reactor = newReactor(selector);
+ try {
+ final NamedEndpoint endpoint = new NamedEndpoint() {
+ @Override
+ public String getHostName() {
+ return "localhost";
+ }
+
+ @Override
+ public int getPort() {
+ return 80;
+ }
+ };
+
+ reactor.connect(endpoint, new InetSocketAddress("localhost", 80), null, Timeout.ofSeconds(1), null, null);
+
+ Assertions.assertEquals(1, reactor.selectWorker().pendingChannelCount());
+ } finally {
+ reactor.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void statusDelegatesToInnerReactor() {
+ final DefaultListeningIOReactor reactor = newReactor(dispatchers -> 0);
+ try {
+ Assertions.assertEquals(IOReactorStatus.INACTIVE, reactor.getStatus());
+ } finally {
+ reactor.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOReactorWorker.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOReactorWorker.java
new file mode 100644
index 0000000000..c84665ac18
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOReactorWorker.java
@@ -0,0 +1,86 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.io.IOException;
+
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestIOReactorWorker {
+
+ private static class TestReactor extends AbstractSingleCoreIOReactor {
+
+ private final RuntimeException runtimeException;
+ private final Error error;
+
+ TestReactor(final RuntimeException runtimeException, final Error error) {
+ super(null);
+ this.runtimeException = runtimeException;
+ this.error = error;
+ }
+
+ @Override
+ public void execute() {
+ if (error != null) {
+ throw error;
+ }
+ if (runtimeException != null) {
+ throw runtimeException;
+ }
+ }
+
+ @Override
+ void doExecute() throws IOException {
+ }
+
+ @Override
+ void doTerminate() throws IOException {
+ }
+ }
+
+ @Test
+ void capturesRuntimeException() {
+ final RuntimeException ex = new RuntimeException("boom");
+ final IOReactorWorker worker = new IOReactorWorker(new TestReactor(ex, null));
+
+ worker.run();
+
+ Assertions.assertSame(ex, worker.getThrowable());
+ }
+
+ @Test
+ void rethrowsError() {
+ final Error err = new AssertionError("fatal");
+ final IOReactorWorker worker = new IOReactorWorker(new TestReactor(null, err));
+
+ final Error thrown = Assertions.assertThrows(Error.class, worker::run);
+ Assertions.assertSame(err, thrown);
+ Assertions.assertSame(err, worker.getThrowable());
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java
new file mode 100644
index 0000000000..3afdf0d03e
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestIOSessionImpl.java
@@ -0,0 +1,141 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.io.CloseMode;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestIOSessionImpl {
+
+ private static class TestCommand implements Command {
+ final AtomicBoolean cancelled = new AtomicBoolean();
+
+ @Override
+ public boolean cancel() {
+ cancelled.set(true);
+ return true;
+ }
+ }
+
+ @Test
+ @SuppressWarnings("resource")
+ void enqueueUpdatesInterestOpsAndOrders() throws Exception {
+ try (Selector selector = Selector.open();
+ SocketChannel channel = SocketChannel.open()) {
+ channel.configureBlocking(false);
+ final SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
+ final AtomicReference closedRef = new AtomicReference<>();
+ final IOSessionImpl session = new IOSessionImpl("t", key, channel, closedRef::set);
+ try {
+ final TestCommand normal = new TestCommand();
+ final TestCommand immediate = new TestCommand();
+
+ session.enqueue(normal, Command.Priority.NORMAL);
+ session.enqueue(immediate, Command.Priority.IMMEDIATE);
+
+ Assertions.assertTrue(session.hasCommands());
+ Assertions.assertEquals(2, session.getPendingCommandCount());
+ Assertions.assertSame(immediate, session.poll());
+ Assertions.assertSame(normal, session.poll());
+
+ Assertions.assertEquals(SelectionKey.OP_READ | SelectionKey.OP_WRITE, key.interestOps());
+ } finally {
+ session.close(CloseMode.IMMEDIATE);
+ Assertions.assertSame(session, closedRef.get());
+ }
+ }
+ }
+
+ @Test
+ @SuppressWarnings("resource")
+ void enqueueOnClosedCancels() throws Exception {
+ try (Selector selector = Selector.open();
+ SocketChannel channel = SocketChannel.open()) {
+ channel.configureBlocking(false);
+ final SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
+ final IOSessionImpl session = new IOSessionImpl("t", key, channel, null);
+ try {
+ session.close(CloseMode.IMMEDIATE);
+
+ final TestCommand command = new TestCommand();
+ session.enqueue(command, Command.Priority.NORMAL);
+
+ Assertions.assertTrue(command.cancelled.get());
+ } finally {
+ session.close(CloseMode.IMMEDIATE);
+ }
+ }
+ }
+
+ @Test
+ @SuppressWarnings("resource")
+ void eventMaskHelpersWork() throws Exception {
+ try (Selector selector = Selector.open();
+ SocketChannel channel = SocketChannel.open()) {
+ channel.configureBlocking(false);
+ final SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
+ final IOSessionImpl session = new IOSessionImpl("t", key, channel, null);
+ try {
+ session.setEvent(SelectionKey.OP_WRITE);
+ Assertions.assertEquals(SelectionKey.OP_READ | SelectionKey.OP_WRITE, session.getEventMask());
+
+ session.clearEvent(SelectionKey.OP_READ);
+ Assertions.assertEquals(SelectionKey.OP_WRITE, session.getEventMask());
+
+ session.setEventMask(SelectionKey.OP_READ);
+ Assertions.assertEquals(SelectionKey.OP_READ, session.getEventMask());
+ } finally {
+ session.close(CloseMode.IMMEDIATE);
+ }
+ }
+ }
+
+ @Test
+ @SuppressWarnings("resource")
+ void toStringIncludesStatus() throws Exception {
+ try (Selector selector = Selector.open();
+ SocketChannel channel = SocketChannel.open()) {
+ channel.configureBlocking(false);
+ final SelectionKey key = channel.register(selector, SelectionKey.OP_READ);
+ final IOSessionImpl session = new IOSessionImpl("t", key, channel, null);
+ try {
+ final String text = session.toString();
+ Assertions.assertTrue(text.contains("ACTIVE"));
+ } finally {
+ session.close(CloseMode.IMMEDIATE);
+ }
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalChannel.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalChannel.java
new file mode 100644
index 0000000000..cfd2356098
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalChannel.java
@@ -0,0 +1,143 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.io.IOException;
+import java.nio.channels.CancelledKeyException;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestInternalChannel {
+
+ private static class TestChannel extends InternalChannel {
+ final AtomicBoolean closed = new AtomicBoolean();
+ final AtomicReference closeMode = new AtomicReference<>();
+ final AtomicBoolean timedOut = new AtomicBoolean();
+ final AtomicReference exceptionRef = new AtomicReference<>();
+ Timeout timeout = Timeout.ofMilliseconds(1);
+ long lastEventTime = 0;
+ boolean throwCancelled;
+ boolean throwRuntime;
+
+ @Override
+ void onIOEvent(final int ops) throws IOException {
+ if (throwCancelled) {
+ throw new CancelledKeyException();
+ }
+ if (throwRuntime) {
+ throw new IOException("boom");
+ }
+ }
+
+ @Override
+ void onTimeout(final Timeout timeout) {
+ timedOut.set(true);
+ }
+
+ @Override
+ void onException(final Exception cause) {
+ exceptionRef.set(cause);
+ }
+
+ @Override
+ Timeout getTimeout() {
+ return timeout;
+ }
+
+ @Override
+ long getLastEventTime() {
+ return lastEventTime;
+ }
+
+ @Override
+ public void close() {
+ closed.set(true);
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ closed.set(true);
+ this.closeMode.set(closeMode);
+ }
+ }
+
+ @Test
+ void handleIoEventClosesOnCancelledKey() {
+ try (TestChannel channel = new TestChannel()) {
+ channel.throwCancelled = true;
+
+ channel.handleIOEvent(0);
+
+ Assertions.assertTrue(channel.closed.get());
+ Assertions.assertEquals(CloseMode.GRACEFUL, channel.closeMode.get());
+ }
+ }
+
+ @Test
+ void handleIoEventClosesOnException() {
+ try (TestChannel channel = new TestChannel()) {
+ channel.throwRuntime = true;
+
+ channel.handleIOEvent(0);
+
+ Assertions.assertTrue(channel.closed.get());
+ Assertions.assertEquals(CloseMode.GRACEFUL, channel.closeMode.get());
+ Assertions.assertNotNull(channel.exceptionRef.get());
+ }
+ }
+
+ @Test
+ void checkTimeoutInvokesCallback() {
+ try (TestChannel channel = new TestChannel()) {
+ channel.lastEventTime = 0;
+ channel.timeout = Timeout.ofMilliseconds(1);
+
+ final boolean result = channel.checkTimeout(10);
+
+ Assertions.assertFalse(result);
+ Assertions.assertTrue(channel.timedOut.get());
+ }
+ }
+
+ @Test
+ void checkTimeoutSkipsWhenDisabled() {
+ try (TestChannel channel = new TestChannel()) {
+ channel.timeout = Timeout.DISABLED;
+
+ final boolean result = channel.checkTimeout(System.currentTimeMillis());
+
+ Assertions.assertTrue(result);
+ Assertions.assertFalse(channel.timedOut.get());
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalConnectChannel.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalConnectChannel.java
new file mode 100644
index 0000000000..d786832152
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalConnectChannel.java
@@ -0,0 +1,134 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.net.InetSocketAddress;
+import java.net.SocketTimeoutException;
+import java.nio.channels.SelectionKey;
+import java.nio.channels.Selector;
+import java.nio.channels.SocketChannel;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestInternalConnectChannel {
+
+ @Test
+ void onTimeoutFailsRequestAndCloses() throws Exception {
+ try (Selector selector = Selector.open();
+ SocketChannel channel = SocketChannel.open()) {
+ channel.configureBlocking(false);
+ final SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT);
+ final AtomicReference failure = new AtomicReference<>();
+ final NamedEndpoint endpoint = new NamedEndpoint() {
+ @Override
+ public String getHostName() {
+ return "example.com";
+ }
+
+ @Override
+ public int getPort() {
+ return 443;
+ }
+ };
+ final IOSessionRequest request = new IOSessionRequest(
+ endpoint,
+ new InetSocketAddress("example.com", 443),
+ null,
+ Timeout.ofMilliseconds(1),
+ null,
+ new FutureCallback() {
+ @Override
+ public void completed(final IOSession result) {
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ failure.set(ex);
+ }
+
+ @Override
+ public void cancelled() {
+ }
+ });
+
+ try (InternalConnectChannel connectChannel = new InternalConnectChannel(
+ key,
+ channel,
+ request,
+ null,
+ null,
+ IOReactorConfig.DEFAULT)) {
+ connectChannel.onTimeout(Timeout.ofMilliseconds(1));
+
+ Assertions.assertTrue(failure.get() instanceof SocketTimeoutException);
+ Assertions.assertFalse(channel.isOpen());
+ }
+ }
+ }
+
+ @Test
+ void toStringUsesRequest() throws Exception {
+ try (Selector selector = Selector.open();
+ SocketChannel channel = SocketChannel.open()) {
+ channel.configureBlocking(false);
+ final SelectionKey key = channel.register(selector, SelectionKey.OP_CONNECT);
+ final NamedEndpoint endpoint = new NamedEndpoint() {
+ @Override
+ public String getHostName() {
+ return "example.com";
+ }
+
+ @Override
+ public int getPort() {
+ return 443;
+ }
+ };
+ final IOSessionRequest request = new IOSessionRequest(
+ endpoint,
+ new InetSocketAddress("example.com", 443),
+ null,
+ Timeout.ofMilliseconds(1),
+ "att",
+ null);
+ try (InternalConnectChannel connectChannel = new InternalConnectChannel(
+ key,
+ channel,
+ request,
+ null,
+ null,
+ IOReactorConfig.DEFAULT)) {
+ Assertions.assertTrue(connectChannel.toString().contains("remoteEndpoint"));
+ }
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalDataChannel.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalDataChannel.java
new file mode 100644
index 0000000000..2b66fd5ed9
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestInternalDataChannel.java
@@ -0,0 +1,148 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.nio.channels.SelectionKey;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+@SuppressWarnings("resource")
+class TestInternalDataChannel {
+
+ @Test
+ void ioEventsNotifyHandlerAndListener() throws Exception {
+ final IOSession session = Mockito.mock(IOSession.class);
+ final IOEventHandler handler = Mockito.mock(IOEventHandler.class);
+ final IOSessionListener listener = Mockito.mock(IOSessionListener.class);
+ Mockito.when(session.getHandler()).thenReturn(handler);
+ Mockito.when(session.getEventMask()).thenReturn(0);
+
+ final InternalDataChannel channel = new InternalDataChannel(session, null, null, listener);
+ channel.upgrade(handler);
+ try {
+ channel.onIOEvent(SelectionKey.OP_CONNECT | SelectionKey.OP_READ | SelectionKey.OP_WRITE);
+
+ Mockito.verify(session).clearEvent(SelectionKey.OP_CONNECT);
+ Mockito.verify(session).updateReadTime();
+ Mockito.verify(session).updateWriteTime();
+ Mockito.verify(listener).connected(session);
+ Mockito.verify(listener).inputReady(session);
+ Mockito.verify(listener).outputReady(session);
+ Mockito.verify(handler).connected(session);
+ Mockito.verify(handler).inputReady(Mockito.eq(session), Mockito.isNull());
+ Mockito.verify(handler).outputReady(session);
+ } finally {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void timeoutNotifiesHandlerAndListener() throws Exception {
+ final IOSession session = Mockito.mock(IOSession.class);
+ final IOEventHandler handler = Mockito.mock(IOEventHandler.class);
+ final IOSessionListener listener = Mockito.mock(IOSessionListener.class);
+ Mockito.when(session.getHandler()).thenReturn(handler);
+
+ final InternalDataChannel channel = new InternalDataChannel(session, null, null, listener);
+ channel.upgrade(handler);
+ try {
+ channel.onTimeout(Timeout.ofSeconds(1));
+
+ Mockito.verify(listener).timeout(session);
+ Mockito.verify(handler).timeout(session, Timeout.ofSeconds(1));
+ } finally {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void registerAndSwitchProtocol() {
+ final IOSession session = Mockito.mock(IOSession.class);
+ final InternalDataChannel channel = new InternalDataChannel(session, null, null, null);
+ final ProtocolUpgradeHandler upgradeHandler = Mockito.mock(ProtocolUpgradeHandler.class);
+ try {
+ channel.registerProtocol("h2", upgradeHandler);
+ channel.switchProtocol("H2", null);
+
+ Mockito.verify(upgradeHandler).upgrade(Mockito.eq(channel), Mockito.isNull());
+ Assertions.assertThrows(IllegalStateException.class, () -> channel.switchProtocol("unknown", null));
+ } finally {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void closeImmediateDelegatesToSession() {
+ final IOSession session = Mockito.mock(IOSession.class);
+ final InternalDataChannel channel = new InternalDataChannel(session, null, null, null);
+ try {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+
+ Mockito.verify(session).close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ } finally {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void exceptionNotifiesListenerAndHandler() {
+ final IOSession session = Mockito.mock(IOSession.class);
+ final IOEventHandler handler = Mockito.mock(IOEventHandler.class);
+ final IOSessionListener listener = Mockito.mock(IOSessionListener.class);
+ Mockito.when(session.getHandler()).thenReturn(handler);
+
+ final InternalDataChannel channel = new InternalDataChannel(session, null, null, listener);
+ final RuntimeException ex = new RuntimeException("boom");
+ try {
+ channel.onException(ex);
+
+ Mockito.verify(listener).exception(session, ex);
+ Mockito.verify(handler).exception(session, ex);
+ } finally {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+ @Test
+ void upgradeHandlerMissingThrows() {
+ final IOSession session = Mockito.mock(IOSession.class);
+ final InternalDataChannel channel = new InternalDataChannel(session, null, null, null);
+ try {
+ @SuppressWarnings("unchecked")
+ final FutureCallback callback =
+ (FutureCallback) Mockito.mock(FutureCallback.class);
+ Assertions.assertThrows(IllegalStateException.class, () -> channel.switchProtocol("h2", callback));
+ } finally {
+ channel.close(org.apache.hc.core5.io.CloseMode.IMMEDIATE);
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestMultiCoreIOReactor.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestMultiCoreIOReactor.java
new file mode 100644
index 0000000000..dc4bbc9a36
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestMultiCoreIOReactor.java
@@ -0,0 +1,104 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.util.concurrent.atomic.AtomicBoolean;
+
+import org.apache.hc.core5.io.CloseMode;
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestMultiCoreIOReactor {
+
+ private static class TestReactor implements IOReactor {
+ final AtomicBoolean shutdown = new AtomicBoolean();
+ final AtomicBoolean closed = new AtomicBoolean();
+ volatile IOReactorStatus status = IOReactorStatus.INACTIVE;
+
+ @Override
+ public void close() {
+ close(CloseMode.GRACEFUL);
+ }
+
+ @Override
+ public void close(final CloseMode closeMode) {
+ closed.set(true);
+ status = IOReactorStatus.SHUT_DOWN;
+ }
+
+ @Override
+ public IOReactorStatus getStatus() {
+ return status;
+ }
+
+ @Override
+ public void initiateShutdown() {
+ shutdown.set(true);
+ status = IOReactorStatus.SHUTTING_DOWN;
+ }
+
+ @Override
+ public void awaitShutdown(final TimeValue waitTime) {
+ status = IOReactorStatus.SHUT_DOWN;
+ }
+
+ }
+
+ @Test
+ void startAndShutdown() throws Exception {
+ final TestReactor r1 = new TestReactor();
+ final TestReactor r2 = new TestReactor();
+ final Thread t1 = new Thread(() -> {
+ });
+ final Thread t2 = new Thread(() -> {
+ });
+
+ try (MultiCoreIOReactor reactor = new MultiCoreIOReactor(
+ new IOReactor[] { r1, r2 }, new Thread[] { t1, t2 })) {
+ reactor.start();
+ Assertions.assertEquals(IOReactorStatus.ACTIVE, reactor.getStatus());
+
+ reactor.initiateShutdown();
+ Assertions.assertTrue(r1.shutdown.get());
+ Assertions.assertTrue(r2.shutdown.get());
+
+ reactor.close(CloseMode.IMMEDIATE);
+ Assertions.assertTrue(r1.closed.get());
+ Assertions.assertTrue(r2.closed.get());
+ Assertions.assertEquals(IOReactorStatus.SHUT_DOWN, reactor.getStatus());
+ }
+ }
+
+ @Test
+ void toStringIncludesStatus() {
+ try (MultiCoreIOReactor reactor = new MultiCoreIOReactor(new IOReactor[] {}, new Thread[] {})) {
+ Assertions.assertTrue(reactor.toString().contains("status"));
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestReactorBasics.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestReactorBasics.java
new file mode 100644
index 0000000000..3e50d0c8e0
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestReactorBasics.java
@@ -0,0 +1,88 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.nio.channels.SelectionKey;
+import java.nio.channels.SocketChannel;
+
+import org.apache.hc.core5.http.HttpHost;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestReactorBasics {
+
+ @Test
+ void channelEntryToString() throws Exception {
+ try (SocketChannel channel = SocketChannel.open()) {
+ final ChannelEntry entry = new ChannelEntry(channel, "att");
+ Assertions.assertTrue(entry.toString().contains("attachment=att"));
+ }
+ }
+
+ @Test
+ void eventMaskConstantsMatchSelectionKey() {
+ Assertions.assertEquals(SelectionKey.OP_READ, EventMask.READ);
+ Assertions.assertEquals(SelectionKey.OP_WRITE, EventMask.WRITE);
+ Assertions.assertEquals(SelectionKey.OP_READ | SelectionKey.OP_WRITE, EventMask.READ_WRITE);
+ }
+
+ @Test
+ void endpointParametersExposeValues() {
+ final EndpointParameters params = new EndpointParameters("https", "example.com", 8443, "tag");
+ Assertions.assertEquals("https", params.getScheme());
+ Assertions.assertEquals("example.com", params.getHostName());
+ Assertions.assertEquals(8443, params.getPort());
+ Assertions.assertEquals("tag", params.getAttachment());
+
+ final EndpointParameters fromHost = new EndpointParameters(new HttpHost("http", "localhost", 80), null);
+ Assertions.assertEquals("http", fromHost.getScheme());
+ Assertions.assertEquals("localhost", fromHost.getHostName());
+ Assertions.assertEquals(80, fromHost.getPort());
+ }
+
+ @Test
+ void ioReactorShutdownExceptionKeepsMessage() {
+ final IOReactorShutdownException ex = new IOReactorShutdownException("down");
+ Assertions.assertEquals("down", ex.getMessage());
+ }
+
+ @Test
+ void ioReactorStatusValuesPresent() {
+ Assertions.assertNotNull(IOReactorStatus.INACTIVE);
+ Assertions.assertNotNull(IOReactorStatus.ACTIVE);
+ Assertions.assertNotNull(IOReactorStatus.SHUTTING_DOWN);
+ Assertions.assertNotNull(IOReactorStatus.SHUT_DOWN);
+ Assertions.assertNotEquals(IOReactorStatus.INACTIVE, IOReactorStatus.ACTIVE);
+ }
+
+ @Test
+ void commandPriorityEnumValuesPresent() {
+ Assertions.assertNotNull(Command.Priority.NORMAL);
+ Assertions.assertNotNull(Command.Priority.IMMEDIATE);
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestSingleCoreIOReactor.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestSingleCoreIOReactor.java
new file mode 100644
index 0000000000..4723f93c6c
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestSingleCoreIOReactor.java
@@ -0,0 +1,94 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+
+import org.apache.hc.core5.function.Decorator;
+import org.apache.hc.core5.net.NamedEndpoint;
+import org.apache.hc.core5.util.Timeout;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
+
+class TestSingleCoreIOReactor {
+
+ private SingleCoreIOReactor newReactor() {
+ final IOEventHandlerFactory factory = Mockito.mock(IOEventHandlerFactory.class);
+ Mockito.when(factory.createHandler(Mockito.any(), Mockito.any())).thenReturn(Mockito.mock(IOEventHandler.class));
+ @SuppressWarnings("unchecked")
+ final Decorator decorator = (Decorator) Mockito.mock(Decorator.class);
+ return new SingleCoreIOReactor(null, factory, IOReactorConfig.DEFAULT, decorator, null, null, null);
+ }
+
+ @Test
+ void enqueueChannelAndPendingCounts() throws Exception {
+ try (SingleCoreIOReactor reactor = newReactor();
+ SocketChannel channel = SocketChannel.open()) {
+ Assertions.assertEquals(0, reactor.pendingChannelCount());
+
+ reactor.enqueueChannel(new ChannelEntry(channel, "att"));
+
+ Assertions.assertEquals(1, reactor.pendingChannelCount());
+ Assertions.assertEquals(0, reactor.totalChannelCount());
+ }
+ }
+
+ @Test
+ void connectEnqueuesRequest() throws Exception {
+ try (SingleCoreIOReactor reactor = newReactor()) {
+ final NamedEndpoint endpoint = new NamedEndpoint() {
+ @Override
+ public String getHostName() {
+ return "localhost";
+ }
+
+ @Override
+ public int getPort() {
+ return 80;
+ }
+ };
+
+ reactor.connect(endpoint, new InetSocketAddress("localhost", 80), null, Timeout.ofSeconds(1), null, null);
+
+ Assertions.assertEquals(1, reactor.pendingChannelCount());
+ }
+ }
+
+ @Test
+ void enqueueAfterShutdownThrows() throws Exception {
+ try (SingleCoreIOReactor reactor = newReactor();
+ SocketChannel channel = SocketChannel.open()) {
+ reactor.initiateShutdown();
+
+ Assertions.assertThrows(IOReactorShutdownException.class, () ->
+ reactor.enqueueChannel(new ChannelEntry(channel, null)));
+ }
+ }
+
+}
diff --git a/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestSingleCoreListeningIOReactor.java b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestSingleCoreListeningIOReactor.java
new file mode 100644
index 0000000000..f8b8598566
--- /dev/null
+++ b/httpcore5/src/test/java/org/apache/hc/core5/reactor/TestSingleCoreListeningIOReactor.java
@@ -0,0 +1,114 @@
+/*
+ * ====================================================================
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you 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.
+ * ====================================================================
+ *
+ * This software consists of voluntary contributions made by many
+ * individuals on behalf of the Apache Software Foundation. For more
+ * information on the Apache Software Foundation, please see
+ * .
+ *
+ */
+package org.apache.hc.core5.reactor;
+
+import java.net.InetSocketAddress;
+import java.nio.channels.SocketChannel;
+import java.util.Set;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.hc.core5.concurrent.FutureCallback;
+import org.apache.hc.core5.util.TimeValue;
+import org.junit.jupiter.api.Assertions;
+import org.junit.jupiter.api.Test;
+
+class TestSingleCoreListeningIOReactor {
+
+ @Test
+ void listenAcceptsConnectionPauseResume() throws Exception {
+ final CountDownLatch accepted = new CountDownLatch(1);
+ final AtomicReference entryRef = new AtomicReference<>();
+ final IOReactorConfig config = IOReactorConfig.custom()
+ .setSelectInterval(TimeValue.ofMilliseconds(10))
+ .build();
+ try (SingleCoreListeningIOReactor reactor = new SingleCoreListeningIOReactor(null, config, entry -> {
+ entryRef.set(entry);
+ accepted.countDown();
+ try {
+ entry.channel.close();
+ } catch (final Exception ignore) {
+ }
+ })) {
+ final Thread reactorThread = new Thread(reactor::execute, "test-reactor");
+ reactorThread.start();
+
+ final AtomicReference endpointRef = new AtomicReference<>();
+ final FutureCallback callback = new FutureCallback() {
+ @Override
+ public void completed(final ListenerEndpoint result) {
+ endpointRef.set(result);
+ }
+
+ @Override
+ public void failed(final Exception ex) {
+ }
+
+ @Override
+ public void cancelled() {
+ }
+ };
+
+ final ListenerEndpoint endpoint = reactor.listen(new InetSocketAddress("localhost", 0), "att", callback)
+ .get(1, TimeUnit.SECONDS);
+ Assertions.assertNotNull(endpoint);
+ Assertions.assertNotNull(endpoint.getAddress());
+
+ try (SocketChannel client = SocketChannel.open()) {
+ client.connect(endpoint.getAddress());
+ }
+
+ Assertions.assertTrue(accepted.await(1, TimeUnit.SECONDS));
+ Assertions.assertNotNull(entryRef.get());
+ Assertions.assertEquals("att", entryRef.get().attachment);
+
+ final Set endpointsBeforePause = reactor.getEndpoints();
+ Assertions.assertFalse(endpointsBeforePause.isEmpty());
+
+ reactor.pause();
+ Assertions.assertTrue(reactor.getEndpoints().isEmpty());
+
+ reactor.resume();
+
+ reactor.initiateShutdown();
+ reactorThread.join(1000);
+ }
+ }
+
+ @Test
+ void listenAfterShutdownThrows() {
+ try (SingleCoreListeningIOReactor reactor = new SingleCoreListeningIOReactor(null, IOReactorConfig.DEFAULT, entry -> {
+ })) {
+ reactor.initiateShutdown();
+
+ Assertions.assertThrows(IOReactorShutdownException.class, () ->
+ reactor.listen(new InetSocketAddress("localhost", 0), null));
+ }
+ }
+
+}