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)); + } + } + +}